#!/usr/bin/env perl =head1 build_zone Build the basic zone for a tCMS site and import it into powerdns. Otherwise, make it a post so you can edit it in the config backend. In general this should not be called outside of Installer.mk. =head2 OPTIONS =head3 subdomain Specify a subdomain, such as 'foo' to add to the domain. May be passed multiple times. =head3 gsv Google site verification string goes into TXT record =head3 cname Specify a cname, such as 'bar' to add to the domain. By default, the cnames 'www', 'mail' and 'chat' are set up, as these are essential tCMS services setup by the makefile before this. May be passed multiple times. =cut use strict; use warnings; no warnings qw{experimental}; use feature qw{signatures state}; use FindBin::libs; use Trog::Config(); use Trog::Zone(); use Trog::Auth; use DNS::Unbound; use Net::DNS::Packet; use Text::Xslate; use Net::IP; use Getopt::Long qw{GetOptionsFromArray}; $ENV{NOHUP} = 1; exit main(@ARGV) unless caller; sub main(@args) { my %options; GetOptionsFromArray(\@args, 'subdomain=s@' => \$options{subdomains}, 'gsv=s' => \$options{gsv}, 'cname=s@' => \$options{cnames}, ); # Paranoia, some versions of getopt don't do this $options{cnames} //= []; $options{subdomains} //=[]; my $domain = Trog::Config->get()->param('general.hostname'); die "Hostname not set in tCMS configuration. Please set this first." unless $domain; my $user = Trog::Auth::primary_user; die "Primary tCMS user not yet set up" unless $user; # Get a flesh start Trog::Zone::delzone($domain); my ($ip) = domain2ips($domain, 'A'); my ($ip6) = domain2ips($domain, 'AAAA'); my $data = { ip => $ip, ip6 => $ip6, ip_reversed => Net::IP->new($ip)->reverse_ip(), ip6_reversed => Net::IP->new($ip6)->reverse_ip(), title => $domain, nameservers => ["ns1.$domain"], subdomains => [map { { name => $_, ip => domain2ips("$_.$domain", "A"), "ip6" => domain2ips("$_.$domain", "AAAA"), nameservers => ["ns1.$_.$domain"] } } @{$options{subdomains}}], cnames => [(qw{www mail chat},@{$options{cnames}})], gsv_string => $options{gsv} // '', version => 0, dkim_pkey => extract_pkey($domain), acme_challenge => get_dns_dcv_string( $domain ), visibility => 'private', acls => [qw{admin}], aliases => [], tags => ['zone'], form => 'dns.tx', callback => "Trog::Routes::TXT::zone", id => undef, created => undef, local_href => "/text/zone/$domain", href => "/text/zone/$domain", user => $user, }; my $zone = Trog::Zone::addzone($data); print $data->{data}; return 0; } sub extract_pkey ( $domain ) { open(my $fh, '<', "/etc/opendkim/keys/$domain/mail.public"); my @lines = map { chomp $_; $_ } readline $fh; close $fh; shift @lines; pop @lines; return join('', @lines); } sub get_dns_dcv_string( $domain ) { return "TODO"; } sub domain2ips( $domain, $type ) { # XXX would be great to use state here, but felipe my $resolver = DNS::Unbound->new(); my $p = $resolver->resolve( $domain, $type )->answer_packet(); my @rrs = Net::DNS::Packet->new( \$p )->answer; my @addr = map { $_->address } @rrs; @addr=(get_local_ip($type)) unless @addr; return @addr; } my $addrout=''; sub get_local_ip( $type ) { $addrout //=qx{ip addr}; return $type eq 'A' ? _ipv4() : _ipv6(); } sub _ipv4 { state $ip; return $ip if $ip; ($ip) = $addrout =~ m{inet\s+([\d|\.|/]+)\s+scope\s+global}gmx; return $ip; } sub _ipv6 { state $ip6; return $ip6 if $ip6; ($ip6) = $addrout =~ m{inet6\s+([a-f|\d|:|/]+)\s+scope\s+global\s+dynamic\s+mngtmpaddr}gmx; # We have to strip the CIDR off of it, or it breaks Net::IP's brain. $ip6 =~ s|/\d+$||; return $ip6; }