Guest User

Untitled

a guest
Apr 20th, 2018
102
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.08 KB | None | 0 0
  1. #!/usr/bin/perl -w
  2.  
  3. package Atomia::DNS::PowerDNSDatabase;
  4.  
  5. use Moose;
  6. use DBI;
  7. use MIME::Base32;
  8. use Digest::SHA qw(sha1);
  9. use strict;
  10. use warnings;
  11.  
  12. has 'config' => (is => 'ro', isa => 'HashRef');
  13. has 'conn' => (is => 'rw', isa => 'Object');
  14. has 'nsec3_iterations' => (is => 'rw', isa => 'Int');
  15. has 'nsec3_salt' => (is => 'rw', isa => 'Str');
  16. has 'nsec3_salt_pres' => (is => 'rw', isa => 'Str');
  17.  
  18. sub BUILD {
  19. my $self = shift;
  20. $self->nsec3_iterations(defined($self->config->{"powerdns_nsec3_iterations"}) ? $self->config->{"powerdns_nsec3_iterations"} : 1);
  21. my $salt = $self->config->{"powerdns_nsec3_salt"} || "ab";
  22. die "powerdns_nsec3_salt should be one byte in hex format, like 7f" unless defined($salt) && $salt =~ /^[0-9A-F]{2}$/i;
  23. $self->nsec3_salt(chr(hex($salt)));
  24. $self->nsec3_salt_pres($salt);
  25. }
  26.  
  27. sub validate_config {
  28. my $self = shift;
  29. my $config = shift;
  30.  
  31. die("you have to specify powerdns_db_hostname") unless defined($config->{"powerdns_db_hostname"});
  32. die("you have to specify powerdns_db_username") unless defined($config->{"powerdns_db_username"});
  33. die("you have to specify powerdns_db_password") unless defined($config->{"powerdns_db_password"});
  34. die("you have to specify powerdns_db_database") unless defined($config->{"powerdns_db_database"});
  35. }
  36.  
  37. sub dbi {
  38. my $self = shift;
  39.  
  40. $self->validate_config($self->config);
  41.  
  42. my $dbh = $self->conn;
  43.  
  44. if (defined($dbh) && $dbh->ping) {
  45. return $dbh;
  46. } else {
  47. my $dbname = $self->config->{"powerdns_db_database"};
  48. my $dbhost = $self->config->{"powerdns_db_hostname"};
  49. my $dbuser = $self->config->{"powerdns_db_username"};
  50. my $dbpass = $self->config->{"powerdns_db_password"};
  51. my $dbport = $self->config->{"powerdns_db_port"} || 3306;
  52.  
  53. my $dsn = "DBI:mysql:database=$dbname;host=$dbhost;port=$dbport";
  54. my @conn_args = ($dsn, $dbuser, $dbpass, { PrintError => 0, PrintWarn => 0 });
  55.  
  56. $dbh = DBI->connect(@conn_args);
  57. die("error connecting to $dbname") unless defined($dbh) && $dbh;
  58.  
  59. $dbh->{"AutoCommit"} = 0;
  60. if ($dbh->{'AutoCommit'}) {
  61. die "error setting disabling autocommit";
  62. }
  63.  
  64. $self->conn($dbh);
  65. return $dbh;
  66. }
  67. }
  68.  
  69. sub parse_record {
  70. my $self = shift;
  71. my $record = shift;
  72. my $nsec_type = shift;
  73. my $zone = shift;
  74. my $name = shift;
  75.  
  76. my $content = $record->{"rdata"};
  77. my $type = $record->{"type"};
  78. my $ttl = $record->{"ttl"};
  79. my $label = $record->{"label"};
  80. my $ordername = '';
  81.  
  82. if ($nsec_type eq 'NSEC') {
  83. $ordername = lc(join(" ", reverse(split(/\./, ($label eq '@' ? '' : $label)))));
  84. } elsif ($nsec_type eq 'NSEC3') {
  85. my $nsec3 = $label eq '@' ? $zone->{"name"} : lc($label . "." . $zone->{"name"});
  86. my @parts = split(/\./, $nsec3);
  87. $nsec3 = join("", map { pack("Ca*", length($_), $_) } @parts) . "\0";
  88. $nsec3 = sha1($nsec3, $self->nsec3_salt);
  89.  
  90. for (my $idx = 0; $idx < $self->nsec3_iterations; $idx++) {
  91. $nsec3 = sha1($nsec3, $self->nsec3_salt);
  92. }
  93.  
  94. $ordername = lc(MIME::Base32::encode($nsec3));
  95. }
  96.  
  97. $ordername = $self->dbi->quote($ordername);
  98.  
  99. my $prio = "NULL";
  100. my $fqdn = $label eq '@' ? $name : $self->dbi->quote($label . "." . $zone->{"name"});
  101. my $auth = ($type eq 'NS' && $label ne '@') ? 0 : 1;
  102.  
  103. if ($type eq "SOA") {
  104. $content =~ s/%serial/$zone->{"changetime"}/g;
  105. $content =~ s/\. / /g;
  106. $content =~ s/^([^ ]* [^\.]*)\./$1\@/;
  107. } elsif ($type =~ /^(CNAME|MX|PTR|NS)$/) {
  108. $content = $content . "." . $zone->{"name"} unless $content =~ /\.$/;
  109. $content =~ s/\.$//;
  110. }
  111.  
  112. if ($type =~ /^(MX|SRV)$/) {
  113. if ($content =~ /^(\d+)\s+(.*)$/) {
  114. $prio = $1;
  115. $content = $2;
  116. } else {
  117. die "bad format of rdata for $type";
  118. }
  119. }
  120.  
  121. return ($fqdn, $type, $content, $ttl, $prio, $auth, $ordername);
  122. }
  123.  
  124. sub add_zone {
  125. my $self = shift;
  126. my $zone = shift;
  127. my $records = shift;
  128. my $zone_type = shift;
  129. my $presigned = shift;
  130. my $nsec_type = shift;
  131.  
  132. die "bad indata to add_zone" unless defined($zone) && ref($zone) eq "HASH" && defined($records) && ref($records) eq "ARRAY";
  133.  
  134. my $filename = '/var/log/deaths.txt';
  135. open(my $fh, '>', $filename) or die "Could not open file '$filename' $!";
  136. print $fh "Attempting to add zone\n";
  137.  
  138.  
  139. $zone_type = 'NATIVE' unless defined($zone_type) && $zone_type eq 'MASTER';
  140. $nsec_type = 'NSEC3NARROW' unless defined($nsec_type) && $nsec_type =~ /^NSEC3?$/i;
  141.  
  142. eval {
  143. my $name = $self->dbi->quote($zone->{"name"});
  144.  
  145. if ($zone_type eq 'MASTER' && defined($presigned) && $presigned) {
  146. my $num_row = $self->dbi->selectrow_arrayref("SELECT COUNT(*) FROM domains WHERE type = 'MASTER' AND name = $name");
  147. die "error checking if presigned MASTER domain is already added" unless defined($num_row) && ref($num_row) eq "ARRAY" && scalar(@$num_row) == 1;
  148. return if $num_row->[0] == 1;
  149. }
  150.  
  151. my $query = "SELECT id, type FROM domains WHERE name = $name";
  152. my $domain = $self->dbi->selectrow_arrayref($query);
  153. my $domain_id = defined($domain) && ref($domain) eq "ARRAY" && scalar(@$domain) == 2 ? $domain->[0] : -1;
  154. my $domain_type = defined($domain) && ref($domain) eq "ARRAY" && scalar(@$domain) == 2 ? $domain->[1] : undef;
  155. my $domain_exists = $domain_id != -1 ? 1 : 0;
  156.  
  157. if ($domain_id == -1) {
  158. $query = "INSERT INTO domains (name, type) VALUES ($name, '$zone_type')";
  159. $self->dbi->do($query) || die "error inserting domain row: $DBI::errstr";
  160. print $fh "insert completed\n";
  161. #$domain_id = $self->dbi->last_insert_id(undef, undef, "domains", undef) || die "error retrieving last_insert_id";
  162. #print $fh "get last insert id completed\n";
  163. my $max_domain_id_ps = $self->dbi->prepare("SELECT max(id) from domains");
  164. $max_domain_id_ps->execute();
  165. $domain_id=$max_domain_id_ps->fetch()->[0];
  166. } elsif ($domain_id != -1 && $zone_type ne $domain_type) {
  167. $query = "UPDATE domains SET type = '$zone_type' WHERE id = $domain_id";
  168. $self->dbi->do($query) || die "error updating zone type: $DBI::errstr";
  169. }
  170.  
  171. my @records_to_insert = ();
  172.  
  173. if ($domain_exists) {
  174. my @db_ids_to_delete = ();
  175.  
  176. my $existing_row_hash = {};
  177. my $rows = $self->dbi->selectall_arrayref("SELECT id, name, type, content, ttl, prio, auth, ordername FROM records WHERE domain_id = $domain_id");
  178. EXISTING_ROW: foreach my $row (@$rows) {
  179. next EXISTING_ROW unless defined($row);
  180.  
  181. 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]));
  182. my $row_key = join "#", @parsed_row;
  183. $existing_row_hash->{$row_key} = $row->[0];
  184. }
  185.  
  186. my $updated_record_hash = {};
  187. RECORD: foreach my $record (@$records) {
  188. next RECORD unless defined($record);
  189.  
  190. my @parsed_row = $self->parse_record($record, $nsec_type, $zone, $name);
  191. my $row_key = join "#", @parsed_row;
  192. $updated_record_hash->{$row_key} = 1;
  193.  
  194. if (!defined($existing_row_hash->{$row_key})) {
  195. push(@records_to_insert, $record);
  196. }
  197. }
  198.  
  199. ROW: foreach my $existing_row_key (keys %$existing_row_hash) {
  200. if (!defined($updated_record_hash->{$existing_row_key})) {
  201. push(@db_ids_to_delete, $existing_row_hash->{$existing_row_key});
  202. }
  203. }
  204.  
  205. if (scalar(@db_ids_to_delete) > 0) {
  206. $query = "DELETE FROM records WHERE id IN (" . join(",", @db_ids_to_delete) . ")";
  207. $self->dbi->do($query) || die "error when removing non existing records in zone: $DBI::errstr";
  208. }
  209. } else {
  210. @records_to_insert = @$records;
  211. print $fh "zone records @$records\n";
  212. }
  213.  
  214. my $num_records = scalar(@records_to_insert);
  215. my $weed_dupes = {};
  216.  
  217. for (my $batch = 0; $batch * 1000 < $num_records; $batch++) {
  218. $query = "INSERT INTO records (domain_id, name, type, content, ttl, prio, auth, ordername) VALUES ";
  219.  
  220. my $first_in_batch = 1;
  221.  
  222. RECORD: for (my $idx = 0; $idx < 1000 && $batch * 1000 + $idx < $num_records; $idx++) {
  223. my $record = $records_to_insert[$batch * 1000 + $idx];
  224. my ($fqdn, $type, $content, $ttl, $prio, $auth, $ordername) = $self->parse_record($record, $nsec_type, $zone, $name);
  225. my $label = $record->{"label"};
  226.  
  227. my $dupe_key = "$label/$type/$content";
  228. next RECORD if exists($weed_dupes->{$dupe_key});
  229. $weed_dupes->{$dupe_key} = 1;
  230.  
  231. $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);
  232. $first_in_batch = 0;
  233. }
  234.  
  235. $self->dbi->do($query) || die "error inserting record batch $batch, query=$query: $DBI::errstr";
  236. }
  237.  
  238. $self->dbi->commit();
  239. };
  240. print $fh "exited eval\n";
  241. if ($@) {
  242. my $exception = $@;
  243. $self->dbi->rollback() || die "error rolling due to exception $exception";
  244. print $fh "caught exception $exception, so rolled back\n";
  245. close $fh;
  246. die "caught exception $exception, rollback successfull";
  247. }
  248. close $fh;
  249. }
  250.  
  251. sub remove_zone {
  252. my $self = shift;
  253. my $zone = shift;
  254.  
  255. eval {
  256. my $name = $self->dbi->quote($zone->{"name"});
  257. $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";
  258. $self->dbi->commit();
  259. };
  260.  
  261. if ($@) {
  262. my $exception = $@;
  263. $self->dbi->rollback() || die "error rolling due to exception $exception";
  264.  
  265. die "caught exception $exception, rollback successfull";
  266. }
  267. }
  268.  
  269. sub set_dnssec_metadata {
  270. my $self = shift;
  271. my $presigned = shift;
  272. my $also_notify = shift;
  273. my $nsec_type = shift;
  274.  
  275. $presigned = 0 if defined($presigned) && $presigned != 1;
  276. $also_notify = '' unless defined($also_notify) && $also_notify =~ /^[\d.]+$/;
  277. $nsec_type = 'NSEC3NARROW' unless defined($nsec_type) && $nsec_type =~ /^NSEC3?$/i;
  278.  
  279. 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";
  280. my $num_metadata = $self->dbi->selectrow_arrayref($query);
  281. die "error checking status of global metadata, query was $query" unless defined($num_metadata) && ref($num_metadata) eq "ARRAY" && scalar(@$num_metadata) == 5;
  282.  
  283. my $db_is_presigned = (($num_metadata->[0] + ($also_notify ne '' ? 1 : 0) == $num_metadata->[1] + $num_metadata->[3]) && $num_metadata->[1] == 1);
  284. my $db_correct_nsec = ($nsec_type eq 'NSEC3NARROW' && $num_metadata->[0] == 3 && $num_metadata->[2] == 2 && $num_metadata->[4] == 1) ||
  285. ($nsec_type eq 'NSEC3' && $num_metadata->[0] == 2 && $num_metadata->[2] == 1 && $num_metadata->[4] == 1) ||
  286. ($nsec_type eq 'NSEC' && $num_metadata->[0] == 1 && $num_metadata->[4] == 1);
  287. my $db_correct_notify = ($num_metadata->[3] == ($also_notify ne '' ? 1 : 0) && $num_metadata->[0] == 1);
  288.  
  289. eval {
  290. if (defined($presigned) && $presigned && !$db_is_presigned) {
  291. $self->dbi->do("DELETE FROM global_domainmetadata");
  292. $self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('PRESIGNED', '1')");
  293. $self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('ALSO-NOTIFY', '$also_notify')") unless $also_notify eq '';
  294. $self->dbi->commit();
  295. } elsif (defined($presigned) && !$presigned && !$db_correct_nsec) {
  296. $self->dbi->do("DELETE FROM global_domainmetadata");
  297. $self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('SOA-EDIT', 'INCEPTION-EPOCH')");
  298. $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';
  299. $self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('NSEC3NARROW', '1')") if $nsec_type eq 'NSEC3NARROW';
  300. $self->dbi->commit();
  301. } elsif (!defined($presigned) && !$db_correct_notify) {
  302. $self->dbi->do("DELETE FROM global_domainmetadata");
  303. $self->dbi->do("INSERT INTO global_domainmetadata (kind, content) VALUES ('ALSO-NOTIFY', '$also_notify')") unless $also_notify eq '';
  304. }
  305. };
  306.  
  307. if ($@) {
  308. my $exception = $@;
  309. $self->dbi->rollback() || die "error rolling due to exception $exception";
  310.  
  311. die "caught exception $exception, rollback successfull";
  312. }
  313. }
  314.  
  315. sub sync_keyset {
  316. my $self = shift;
  317. my $keyset = shift;
  318.  
  319. my $keys_in_db = $self->dbi->selectall_arrayref("SELECT * FROM global_cryptokeys", { Slice => {} });
  320. die "error fetching crypto keys" unless defined($keys_in_db) && ref($keys_in_db) eq "ARRAY" && !$DBI::err;
  321.  
  322. eval {
  323. my $changed = 0;
  324.  
  325. CHECK_KEYS_TO_ADD: foreach my $key (@$keyset) {
  326. my $keydata = $key->{"keydata"};
  327. my $id = $key->{"id"};
  328. die "key from atomia dns has bad format" unless $keydata =~ /^Private-key-format/ && $id =~ /^\d+$/;
  329.  
  330. foreach my $dbkey (@$keys_in_db) {
  331. if ($dbkey->{"content"} eq $keydata) {
  332. next CHECK_KEYS_TO_ADD;
  333. }
  334. }
  335.  
  336. my $flags = $key->{"keytype"} eq "KSK" ? 257 : 256;
  337. my $active = $key->{"activated"} == 1 ? 1 : 0;
  338. $keydata = $self->dbi->quote($keydata);
  339.  
  340. $self->dbi->do("INSERT INTO global_cryptokeys (id, flags, active, content) VALUES ($id, $flags, $active, $keydata)") || die "error inserting key into database: $DBI::errstr";
  341. $changed = 1;
  342. }
  343.  
  344. CHECK_KEYS_TO_REMOVE: foreach my $key (@$keys_in_db) {
  345. my $keydata = $key->{"content"};
  346. my $id = $key->{"id"};
  347. die "key from database has bad format" unless $keydata =~ /^Private-key-format/ && $id =~ /^\d+$/;
  348.  
  349. foreach my $soapkey (@$keyset) {
  350. if ($soapkey->{"keydata"} eq $keydata) {
  351. next CHECK_KEYS_TO_REMOVE;
  352. }
  353. }
  354.  
  355. $self->dbi->do("DELETE FROM global_cryptokeys WHERE id = $id") || die "error removing key from database: $DBI::errstr";
  356. $changed = 1;
  357. }
  358.  
  359. $self->dbi->commit() if $changed;
  360. };
  361.  
  362. if ($@) {
  363. my $exception = $@;
  364. $self->dbi->rollback() || die "error rolling due to exception $exception";
  365.  
  366. die "caught exception $exception, rollback successfull";
  367. }
  368. }
  369.  
  370. sub add_slave_zone {
  371. my $self = shift;
  372. my $zone = shift;
  373. my $options = shift;
  374.  
  375. die "bad indata to add_zone" unless defined($zone) && ref($zone) eq "" && $zone =~ /^[a-z0-9.-]+$/ && defined($options) && ref($options) eq "HASH";
  376. die "invalid master" unless defined($options->{"master"}) && length($options->{"master"}) > 0;
  377.  
  378. eval {
  379. my $name = $self->dbi->quote($zone);
  380. my $master = $self->dbi->quote($options->{"master"});
  381. my $tsig = $options->{"tsig_secret"};
  382. $tsig = undef if defined($tsig) && $tsig eq '';
  383. $tsig = $self->dbi->quote($tsig) if defined($tsig);
  384.  
  385. my $tsig_name = $options->{"tsig_name"};
  386. $tsig_name = undef if defined($tsig_name) && $tsig_name eq '';
  387. $tsig_name = $self->dbi->quote($tsig_name) if defined($tsig_name);
  388. $tsig_name = "NULL" unless defined($tsig_name);
  389.  
  390. $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";
  391.  
  392. my $query = "INSERT INTO domains (name, type, master) VALUES ($name, 'SLAVE', $master)";
  393.  
  394. $self->dbi->do($query) || die "error inserting domain row: $DBI::errstr";
  395.  
  396. if (defined($tsig)) {
  397. my $domain_id = $self->dbi->last_insert_id(undef, undef, "domains", undef) || die "error retrieving last_insert_id";
  398. $query = "INSERT INTO outbound_tsig_keys (domain_id, secret, name) VALUES ($domain_id, $tsig, $tsig_name)";
  399. $self->dbi->do($query) || die "error inserting tsig row using $query: $DBI::errstr";
  400. }
  401.  
  402. $self->dbi->commit();
  403. };
  404.  
  405. if ($@) {
  406. my $exception = $@;
  407. $self->dbi->rollback() || die "error rolling due to exception $exception";
  408.  
  409. die "caught exception $exception, rollback successfull";
  410. }
  411. }
  412.  
  413. sub remove_slave_zone {
  414. my $self = shift;
  415. my $zonename = shift;
  416.  
  417. eval {
  418. my $name = $self->dbi->quote($zonename);
  419. $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";
  420. $self->dbi->commit();
  421. };
  422.  
  423. if ($@) {
  424. my $exception = $@;
  425. $self->dbi->rollback() || die "error rolling due to exception $exception";
  426.  
  427. die "caught exception $exception, rollback successfull";
  428. }
  429. }
  430.  
  431. 1;
Add Comment
Please, Sign In to add comment