Created
April 12, 2018 12:02
-
-
Save avigail-oron/910555af4b22bdde0a5a970cb8572956 to your computer and use it in GitHub Desktop.
fixed file. replace get_last_insert with select max(id)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl -w | |
package Atomia::DNS::PowerDNSDatabase; | |
use Moose; | |
use DBI; | |
use MIME::Base32; | |
use Digest::SHA qw(sha1); | |
use strict; | |
use warnings; | |
has 'config' => (is => 'ro', isa => 'HashRef'); | |
has 'conn' => (is => 'rw', isa => 'Object'); | |
has 'nsec3_iterations' => (is => 'rw', isa => 'Int'); | |
has 'nsec3_salt' => (is => 'rw', isa => 'Str'); | |
has 'nsec3_salt_pres' => (is => 'rw', isa => 'Str'); | |
sub BUILD { | |
my $self = shift; | |
$self->nsec3_iterations(defined($self->config->{"powerdns_nsec3_iterations"}) ? $self->config->{"powerdns_nsec3_iterations"} : 1); | |
my $salt = $self->config->{"powerdns_nsec3_salt"} || "ab"; | |
die "powerdns_nsec3_salt should be one byte in hex format, like 7f" unless defined($salt) && $salt =~ /^[0-9A-F]{2}$/i; | |
$self->nsec3_salt(chr(hex($salt))); | |
$self->nsec3_salt_pres($salt); | |
} | |
sub validate_config { | |
my $self = shift; | |
my $config = shift; | |
die("you have to specify powerdns_db_hostname") unless defined($config->{"powerdns_db_hostname"}); | |
die("you have to specify powerdns_db_username") unless defined($config->{"powerdns_db_username"}); | |
die("you have to specify powerdns_db_password") unless defined($config->{"powerdns_db_password"}); | |
die("you have to specify powerdns_db_database") unless defined($config->{"powerdns_db_database"}); | |
} | |
sub dbi { | |
my $self = shift; | |
$self->validate_config($self->config); | |
my $dbh = $self->conn; | |
if (defined($dbh) && $dbh->ping) { | |
return $dbh; | |
} else { | |
my $dbname = $self->config->{"powerdns_db_database"}; | |
my $dbhost = $self->config->{"powerdns_db_hostname"}; | |
my $dbuser = $self->config->{"powerdns_db_username"}; | |
my $dbpass = $self->config->{"powerdns_db_password"}; | |
my $dbport = $self->config->{"powerdns_db_port"} || 3306; | |
my $dsn = "DBI:mysql:database=$dbname;host=$dbhost;port=$dbport"; | |
my @conn_args = ($dsn, $dbuser, $dbpass, { PrintError => 0, PrintWarn => 0 }); | |
$dbh = DBI->connect(@conn_args); | |
die("error connecting to $dbname") unless defined($dbh) && $dbh; | |
$dbh->{"AutoCommit"} = 0; | |
if ($dbh->{'AutoCommit'}) { | |
die "error setting disabling autocommit"; | |
} | |
$self->conn($dbh); | |
return $dbh; | |
} | |
} | |
sub parse_record { | |
my $self = shift; | |
my $record = shift; | |
my $nsec_type = shift; | |
my $zone = shift; | |
my $name = shift; | |
my $content = $record->{"rdata"}; | |
my $type = $record->{"type"}; | |
my $ttl = $record->{"ttl"}; | |
my $label = $record->{"label"}; | |
my $ordername = ''; | |
if ($nsec_type eq 'NSEC') { | |
$ordername = lc(join(" ", reverse(split(/\./, ($label eq '@' ? '' : $label))))); | |
} elsif ($nsec_type eq 'NSEC3') { | |
my $nsec3 = $label eq '@' ? $zone->{"name"} : lc($label . "." . $zone->{"name"}); | |
my @parts = split(/\./, $nsec3); | |
$nsec3 = join("", map { pack("Ca*", length($_), $_) } @parts) . "\0"; | |
$nsec3 = sha1($nsec3, $self->nsec3_salt); | |
for (my $idx = 0; $idx < $self->nsec3_iterations; $idx++) { | |
$nsec3 = sha1($nsec3, $self->nsec3_salt); | |
} | |
$ordername = lc(MIME::Base32::encode($nsec3)); | |
} | |
$ordername = $self->dbi->quote($ordername); | |
my $prio = "NULL"; | |
my $fqdn = $label eq '@' ? $name : $self->dbi->quote($label . "." . $zone->{"name"}); | |
my $auth = ($type eq 'NS' && $label ne '@') ? 0 : 1; | |
if ($type eq "SOA") { | |
$content =~ s/%serial/$zone->{"changetime"}/g; | |
$content =~ s/\. / /g; | |
$content =~ s/^([^ ]* [^\.]*)\./$1\@/; | |
} elsif ($type =~ /^(CNAME|MX|PTR|NS)$/) { | |
$content = $content . "." . $zone->{"name"} unless $content =~ /\.$/; | |
$content =~ s/\.$//; | |
} | |
if ($type =~ /^(MX|SRV)$/) { | |
if ($content =~ /^(\d+)\s+(.*)$/) { | |
$prio = $1; | |
$content = $2; | |
} else { | |
die "bad format of rdata for $type"; | |
} | |
} | |
return ($fqdn, $type, $content, $ttl, $prio, $auth, $ordername); | |
} | |
sub add_zone { | |
my $self = shift; | |
my $zone = shift; | |
my $records = shift; | |
my $zone_type = shift; | |
my $presigned = shift; | |
my $nsec_type = shift; | |
die "bad indata to add_zone" unless defined($zone) && ref($zone) eq "HASH" && defined($records) && ref($records) eq "ARRAY"; | |
my $filename = '/var/log/deaths.txt'; | |
open(my $fh, '>', $filename) or die "Could not open file '$filename' $!"; | |
print $fh "Attempting to add zone\n"; | |
$zone_type = 'NATIVE' unless defined($zone_type) && $zone_type eq 'MASTER'; | |
$nsec_type = 'NSEC3NARROW' unless defined($nsec_type) && $nsec_type =~ /^NSEC3?$/i; | |
eval { | |
my $name = $self->dbi->quote($zone->{"name"}); | |
if ($zone_type eq 'MASTER' && defined($presigned) && $presigned) { | |
my $num_row = $self->dbi->selectrow_arrayref("SELECT COUNT(*) FROM domains WHERE type = 'MASTER' AND name = $name"); | |
die "error checking if presigned MASTER domain is already added" unless defined($num_row) && ref($num_row) eq "ARRAY" && scalar(@$num_row) == 1; | |
return if $num_row->[0] == 1; | |
} | |
my $query = "SELECT id, type FROM domains WHERE name = $name"; | |
my $domain = $self->dbi->selectrow_arrayref($query); | |
my $domain_id = defined($domain) && ref($domain) eq "ARRAY" && scalar(@$domain) == 2 ? $domain->[0] : -1; | |
my $domain_type = defined($domain) && ref($domain) eq "ARRAY" && scalar(@$domain) == 2 ? $domain->[1] : undef; | |
my $domain_exists = $domain_id != -1 ? 1 : 0; | |
if ($domain_id == -1) { | |
$query = "INSERT INTO domains (name, type) VALUES ($name, '$zone_type')"; | |
$self->dbi->do($query) || die "error inserting domain row: $DBI::errstr"; | |
print $fh "insert completed\n"; | |
#$domain_id = $self->dbi->last_insert_id(undef, undef, "domains", undef) || die "error retrieving last_insert_id"; | |
#print $fh "get last insert id completed\n"; | |
my $max_domain_id_ps = $self->dbi->prepare("SELECT max(id) from domains"); | |
$max_domain_id_ps->execute(); | |
$domain_id=$max_domain_id_ps->fetch()->[0]; | |
} elsif ($domain_id != -1 && $zone_type ne $domain_type) { | |
$query = "UPDATE domains SET type = '$zone_type' WHERE id = $domain_id"; | |
$self->dbi->do($query) || die "error updating zone type: $DBI::errstr"; | |
} | |
my @records_to_insert = (); | |
if ($domain_exists) { | |
my @db_ids_to_delete = (); | |
my $existing_row_hash = {}; | |
my $rows = $self->dbi->selectall_arrayref("SELECT id, name, type, content, ttl, prio, auth, ordername FROM records WHERE domain_id = $domain_id"); | |
EXISTING_ROW: foreach my $row (@$rows) { | |
next EXISTING_ROW unless defined($row); | |
my @parsed_row = ($self->dbi->quote($row->[1]), $row->[2], $row->[3], $row->[4], defined($row->[5]) ? $row->[5] : "NULL", $row->[6], $self->dbi->quote($row->[7])); | |
my $row_key = join "#", @parsed_row; | |
$existing_row_hash->{$row_key} = $row->[0]; | |
} | |
my $updated_record_hash = {}; | |
RECORD: foreach my $record (@$records) { | |
next RECORD unless defined($record); | |
my @parsed_row = $self->parse_record($record, $nsec_type, $zone, $name); | |
my $row_key = join "#", @parsed_row; | |
$updated_record_hash->{$row_key} = 1; | |
if (!defined($existing_row_hash->{$row_key})) { | |
push(@records_to_insert, $record); | |
} | |
} | |
ROW: foreach my $existing_row_key (keys %$existing_row_hash) { | |
if (!defined($updated_record_hash->{$existing_row_key})) { | |
push(@db_ids_to_delete, $existing_row_hash->{$existing_row_key}); | |
} | |
} | |
if (scalar(@db_ids_to_delete) > 0) { | |
$query = "DELETE FROM records WHERE id IN (" . join(",", @db_ids_to_delete) . ")"; | |
$self->dbi->do($query) || die "error when removing non existing records in zone: $DBI::errstr"; | |
} | |
} else { | |
@records_to_insert = @$records; | |
print $fh "zone records @$records\n"; | |
} | |
my $num_records = scalar(@records_to_insert); | |
my $weed_dupes = {}; | |
for (my $batch = 0; $batch * 1000 < $num_records; $batch++) { | |
$query = "INSERT INTO records (domain_id, name, type, content, ttl, prio, auth, ordername) VALUES "; | |
my $first_in_batch = 1; | |
RECORD: for (my $idx = 0; $idx < 1000 && $batch * 1000 + $idx < $num_records; $idx++) { | |
my $record = $records_to_insert[$batch * 1000 + $idx]; | |
my ($fqdn, $type, $content, $ttl, $prio, $auth, $ordername) = $self->parse_record($record, $nsec_type, $zone, $name); | |
my $label = $record->{"label"}; | |
my $dupe_key = "$label/$type/$content"; | |
next RECORD if exists($weed_dupes->{$dupe_key}); | |
$weed_dupes->{$dupe_key} = 1; | |
$query .= sprintf("%s(%d, %s, %s, %s, %d, %s, %d, %s)", ($first_in_batch ? '' : ','), $domain_id, $fqdn, $self->dbi->quote($type), $self->dbi->quote($content), $ttl, $prio, $auth, $ordername); | |
$first_in_batch = 0; | |
} | |
$self->dbi->do($query) || die "error inserting record batch $batch, query=$query: $DBI::errstr"; | |
} | |
$self->dbi->commit(); | |
}; | |
print $fh "exited eval\n"; | |
if ($@) { | |
my $exception = $@; | |
$self->dbi->rollback() || die "error rolling due to exception $exception"; | |
print $fh "caught exception $exception, so rolled back\n"; | |
close $fh; | |
die "caught exception $exception, rollback successfull"; | |
} | |
close $fh; | |
} | |
sub remove_zone { | |
my $self = shift; | |
my $zone = shift; | |
eval { | |
my $name = $self->dbi->quote($zone->{"name"}); | |
$self->dbi->do("DELETE domains, records FROM domains INNER JOIN records ON domains.id = records.domain_id WHERE domains.name = $name") || die "error removing zone: $DBI::errstr"; | |
$self->dbi->commit(); | |
}; | |
if ($@) { | |
my $exception = $@; | |
$self->dbi->rollback() || die "error rolling due to exception $exception"; | |
die "caught exception $exception, rollback successfull"; | |
} | |
} | |
sub set_dnssec_metadata { | |
my $self = shift; | |
my $presigned = shift; | |
my $also_notify = shift; | |
my $nsec_type = shift; | |
$presigned = 0 if defined($presigned) && $presigned != 1; | |
$also_notify = '' unless defined($also_notify) && $also_notify =~ /^[\d.]+$/; | |
$nsec_type = 'NSEC3NARROW' unless defined($nsec_type) && $nsec_type =~ /^NSEC3?$/i; | |
my $query = "SELECT COUNT(*), COUNT(IF(kind = 'PRESIGNED', 1, NULL)), COUNT(IF(kind LIKE 'NSEC%', 1, NULL)), COUNT(IF(kind = 'ALSO-NOTIFY' AND content = '$also_notify', 1, NULL)), COUNT(IF(kind = 'SOA-EDIT', 1, NULL)) FROM global_domainmetadata"; | |
my $num_metadata = $self->dbi->selectrow_arrayref($query); | |
die "error checking status of global metadata, query was $query" unless defined($num_metadata) && ref($num_metadata) eq "ARRAY" && scalar(@$num_metadata) == 5; | |
my $db_is_presigned = (($num_metadata->[0] + ($also_notify ne '' ? 1 : 0) == $num_metadata->[1] + $num_metadata->[3]) && $num_metadata->[1] == 1); | |
my $db_correct_nsec = ($nsec_type eq 'NSEC3NARROW' && $num_metadata->[0] == 3 && $num_metadata->[2] == 2 && $num_metadata->[4] == 1) || | |
($nsec_type eq 'NSEC3' && $num_metadata->[0] == 2 && $num_metadata->[2] == 1 && $num_metadata->[4] == 1) || | |
($nsec_type eq 'NSEC' && $num_metadata->[0] == 1 && $num_metadata->[4] == 1); | |
my $db_correct_notify = ($num_metadata->[3] == ($also_notify ne '' ? 1 : 0) && $num_metadata->[0] == 1); | |
eval { | |
if (defined($presigned) && $presigned && !$db_is_presigned) { | |
$self->dbi->do("DELETE FROM global_domainmetadata"); | |
$self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('PRESIGNED', '1')"); | |
$self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('ALSO-NOTIFY', '$also_notify')") unless $also_notify eq ''; | |
$self->dbi->commit(); | |
} elsif (defined($presigned) && !$presigned && !$db_correct_nsec) { | |
$self->dbi->do("DELETE FROM global_domainmetadata"); | |
$self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('SOA-EDIT', 'INCEPTION-EPOCH')"); | |
$self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('NSEC3PARAM', '1 1 " . $self->nsec3_iterations . " " . $self->nsec3_salt_pres . "')") if $nsec_type ne 'NSEC'; | |
$self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('NSEC3NARROW', '1')") if $nsec_type eq 'NSEC3NARROW'; | |
$self->dbi->commit(); | |
} elsif (!defined($presigned) && !$db_correct_notify) { | |
$self->dbi->do("DELETE FROM global_domainmetadata"); | |
$self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('ALSO-NOTIFY', '$also_notify')") unless $also_notify eq ''; | |
} | |
}; | |
if ($@) { | |
my $exception = $@; | |
$self->dbi->rollback() || die "error rolling due to exception $exception"; | |
die "caught exception $exception, rollback successfull"; | |
} | |
} | |
sub sync_keyset { | |
my $self = shift; | |
my $keyset = shift; | |
my $keys_in_db = $self->dbi->selectall_arrayref("SELECT * FROM global_cryptokeys", { Slice => {} }); | |
die "error fetching crypto keys" unless defined($keys_in_db) && ref($keys_in_db) eq "ARRAY" && !$DBI::err; | |
eval { | |
my $changed = 0; | |
CHECK_KEYS_TO_ADD: foreach my $key (@$keyset) { | |
my $keydata = $key->{"keydata"}; | |
my $id = $key->{"id"}; | |
die "key from atomia dns has bad format" unless $keydata =~ /^Private-key-format/ && $id =~ /^\d+$/; | |
foreach my $dbkey (@$keys_in_db) { | |
if ($dbkey->{"content"} eq $keydata) { | |
next CHECK_KEYS_TO_ADD; | |
} | |
} | |
my $flags = $key->{"keytype"} eq "KSK" ? 257 : 256; | |
my $active = $key->{"activated"} == 1 ? 1 : 0; | |
$keydata = $self->dbi->quote($keydata); | |
$self->dbi->do("INSERT INTO global_cryptokeys (id, flags, active, content) VALUES ($id, $flags, $active, $keydata)") || die "error inserting key into database: $DBI::errstr"; | |
$changed = 1; | |
} | |
CHECK_KEYS_TO_REMOVE: foreach my $key (@$keys_in_db) { | |
my $keydata = $key->{"content"}; | |
my $id = $key->{"id"}; | |
die "key from database has bad format" unless $keydata =~ /^Private-key-format/ && $id =~ /^\d+$/; | |
foreach my $soapkey (@$keyset) { | |
if ($soapkey->{"keydata"} eq $keydata) { | |
next CHECK_KEYS_TO_REMOVE; | |
} | |
} | |
$self->dbi->do("DELETE FROM global_cryptokeys WHERE id = $id") || die "error removing key from database: $DBI::errstr"; | |
$changed = 1; | |
} | |
$self->dbi->commit() if $changed; | |
}; | |
if ($@) { | |
my $exception = $@; | |
$self->dbi->rollback() || die "error rolling due to exception $exception"; | |
die "caught exception $exception, rollback successfull"; | |
} | |
} | |
sub add_slave_zone { | |
my $self = shift; | |
my $zone = shift; | |
my $options = shift; | |
die "bad indata to add_zone" unless defined($zone) && ref($zone) eq "" && $zone =~ /^[a-z0-9.-]+$/ && defined($options) && ref($options) eq "HASH"; | |
die "invalid master" unless defined($options->{"master"}) && length($options->{"master"}) > 0; | |
eval { | |
my $name = $self->dbi->quote($zone); | |
my $master = $self->dbi->quote($options->{"master"}); | |
my $tsig = $options->{"tsig_secret"}; | |
$tsig = undef if defined($tsig) && $tsig eq ''; | |
$tsig = $self->dbi->quote($tsig) if defined($tsig); | |
my $tsig_name = $options->{"tsig_name"}; | |
$tsig_name = undef if defined($tsig_name) && $tsig_name eq ''; | |
$tsig_name = $self->dbi->quote($tsig_name) if defined($tsig_name); | |
$tsig_name = "NULL" unless defined($tsig_name); | |
$self->dbi->do("DELETE domains, records FROM domains LEFT JOIN records ON domains.id = records.domain_id WHERE domains.name = $name") || die "error removing previous version of zone in add_zone: $DBI::errstr"; | |
my $query = "INSERT INTO domains (name, type, master) VALUES ($name, 'SLAVE', $master)"; | |
$self->dbi->do($query) || die "error inserting domain row: $DBI::errstr"; | |
if (defined($tsig)) { | |
my $domain_id = $self->dbi->last_insert_id(undef, undef, "domains", undef) || die "error retrieving last_insert_id"; | |
$query = "INSERT INTO outbound_tsig_keys (domain_id, secret, name) VALUES ($domain_id, $tsig, $tsig_name)"; | |
$self->dbi->do($query) || die "error inserting tsig row using $query: $DBI::errstr"; | |
} | |
$self->dbi->commit(); | |
}; | |
if ($@) { | |
my $exception = $@; | |
$self->dbi->rollback() || die "error rolling due to exception $exception"; | |
die "caught exception $exception, rollback successfull"; | |
} | |
} | |
sub remove_slave_zone { | |
my $self = shift; | |
my $zonename = shift; | |
eval { | |
my $name = $self->dbi->quote($zonename); | |
$self->dbi->do("DELETE domains, records, k FROM domains LEFT JOIN records ON domains.id = records.domain_id LEFT JOIN outbound_tsig_keys k ON k.domain_id = domains.id WHERE domains.name = $name") || die "error removing zone: $DBI::errstr"; | |
$self->dbi->commit(); | |
}; | |
if ($@) { | |
my $exception = $@; | |
$self->dbi->rollback() || die "error rolling due to exception $exception"; | |
die "caught exception $exception, rollback successfull"; | |
} | |
} | |
1; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment