Zone.pm 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package Trog::Zone;
  2. =head1 Trog::Zone
  3. =head2 DESCRIPTION
  4. Zonefile CRUD
  5. =cut
  6. use strict;
  7. use warnings;
  8. use feature qw{signatures};
  9. no warnings qw{experimental};
  10. use Trog::Config;
  11. use Trog::Data;
  12. use Trog::Vars;
  13. use Trog::SQLite;
  14. use Net::IP;
  15. use Ref::Util;
  16. =head2 zone($domain) = @zonedata
  17. Returns the zone data for the requested zone.
  18. Like any other post in TCMS it's versioned.
  19. =cut
  20. sub zone ( $domain, $version = undef ) {
  21. my $conf = Trog::Config::get();
  22. my $data = Trog::Data->new($conf);
  23. my @zonedata = $data->get( tags => ['zone'], acls => [qw{admin}], title => $domain );
  24. @zonedata = grep { $_->{version} == $version } @zonedata if defined $version;
  25. return @zonedata;
  26. }
  27. =head2 addzone($domain, %options)
  28. Add a post of 'zone' type.
  29. =cut
  30. my $valid_ip = sub {
  31. return Net::IP->new(shift);
  32. };
  33. my $valid_rev_ip = sub {
  34. return shift =~ m/\.in-addr\.arpa\.$/;
  35. };
  36. my $valid_rev_ip6 = sub {
  37. return shift =~ m/\.ip6\.arpa\.$/;
  38. };
  39. my $spec = {
  40. ip => $valid_ip,
  41. ip6 => $valid_ip,
  42. ip_reversed => $valid_rev_ip,
  43. ip6_reversed => $valid_rev_ip6,
  44. nameservers => \&Ref::Util::is_arrayref,
  45. subdomains => \&Ref::Util::is_arrayref,
  46. cnames => \&Ref::Util::is_arrayref,
  47. gsv_string => $Trog::Vars::not_ref,
  48. dkim_pkey => $Trog::Vars::not_ref,
  49. acme_challenge => $Trog::Vars::not_ref,
  50. };
  51. sub addzone ($query) {
  52. my $domain = $query->{title};
  53. return unless $domain;
  54. my ($latest) = zone($domain);
  55. $latest //= {};
  56. my $conf = Trog::Config::get();
  57. my $data = Trog::Data->new($conf);
  58. #XXX TODO make this instead use @records2add, complexity demon BAD
  59. my $processor = Text::Xslate->new( path => 'www/templates/text' );
  60. $query->{data} = $processor->render( 'zone.tx', $query );
  61. %$latest = (
  62. %$latest,
  63. Trog::Vars::filter( $query, $spec ),
  64. );
  65. $data->add($latest);
  66. #import into pdns
  67. my ( $ttl, $prio, $disabled ) = ( 300, 0, 0 );
  68. my $insert_sql = q{insert into records (domain_id, name, type,content,ttl,prio,disabled) select id , ?, ?, ?, ?, ?, ? from domains where name=?};
  69. my @records2add = (
  70. [ $query->{title}, 'SOA', "$query->{title} soa.$query->{title} $query->{version} 10800 3600 604800 10800" ],
  71. [ $query->{title}, 'A', $query->{ip} ],
  72. [ $query->{title}, 'AAAA', $query->{ip6} ],
  73. [ $query->{ip_reversed}, 'PTR', $query->{title} ],
  74. [ $query->{ip6_reversed}, 'PTR', $query->{title} ],
  75. [ $query->{title}, 'MX', "mail.$query->{title}" ],
  76. [ "_smtps._tcp.mail.$query->{title}", 'SRV', "5 587 ." ],
  77. [ "_imaps._tcp.mail.$query->{title}", 'SRV', "5 993 ." ],
  78. [ "_pop3s._tcp.mail.$query->{title}", 'SRV', "5 995 ." ],
  79. [ "_dmarc.$query->{title}", 'TXT', "v=DMARC1; p=reject; rua=mailto:postmaster\@$query->{title}; ruf=mailto:postmaster\@$query->{title}" ],
  80. [ "mail._domainkey.$query->{title}", 'TXT', "v=DKIM1; h=sha256; k=rsa; t=y; p=$query->{dkim_pkey}" ],
  81. [ $query->{title}, 'TXT', "v=spf1 +mx +a +ip4:$query->{ip} +ip6:$query->{ip6} -all" ],
  82. [ $query->{title}, 'TXT', "google-site-verification=$query->{gsv_string}" ],
  83. [ "_acme-challenge.$query->{title}", 'TXT', $query->{acme_challenge} ],
  84. [ $query->{title}, 'CAA', '0 issue "letsencrypt.org"' ],
  85. );
  86. push( @records2add, ( map { [ "$_.$query->{title}", "CNAME", $query->{title} ] } @{ $query->{cnames} } ) );
  87. push( @records2add, ( map { [ $query->{title}, 'NS', $_ ] } @{ $query->{nameservers} } ) );
  88. foreach my $subdomain ( @{ $query->{subdomains} } ) {
  89. push( @records2add, [ "$subdomain->{name}.$query->{title}", 'A', $subdomain->{ip} ] );
  90. push( @records2add, [ "$subdomain->{name}.$query->{title}", 'AAAA', $subdomain->{ip6} ] );
  91. push( @records2add, ( map { [ "$subdomain->{name}.$query->{title}", 'NS', $_ ] } @{ $subdomain->{nameservers} } ) );
  92. }
  93. my $dbh = _dbh();
  94. $dbh->begin_work();
  95. $dbh->do("DELETE FROM records") or _roll_and_die($dbh);
  96. foreach my $record (@records2add) {
  97. $dbh->do( $insert_sql, undef, @$record, $ttl, $prio, $disabled, $query->{title} ) or _roll_and_die($dbh);
  98. }
  99. $dbh->commit() or _roll_and_die($dbh);
  100. return $latest;
  101. }
  102. sub delzone ($domain) {
  103. my $conf = Trog::Config::get();
  104. my $data = Trog::Data->new($conf);
  105. my ($latest) = zone($domain);
  106. return unless $latest;
  107. return $data->delete($latest);
  108. }
  109. sub _dbh {
  110. return Trog::SQLite::dbh( undef, "dns/zones.db" );
  111. }
  112. sub _roll_and_die ($dbh) {
  113. my $err = $dbh->errstr;
  114. $dbh->rollback();
  115. die $err;
  116. }
  117. 1;