X-Git-Url: https://the.earth.li/gitweb/?a=blobdiff_plain;f=autodns.pl;h=df5f38e54ce727eb3566c25c0160d0d1147a4dcb;hb=HEAD;hp=cfa9510286d8bf178dda0fe4ad94d1933107fad8;hpb=7cff990ef4df44469d5bed817fda2292931a74ae;p=autodns.git diff --git a/autodns.pl b/autodns.pl index cfa9510..df5f38e 100755 --- a/autodns.pl +++ b/autodns.pl @@ -1,27 +1,29 @@ #!/usr/bin/perl -Tw -# autodns 0.0.7 -# Copyright 1999-2004 Project Purple. Written by Jonathan McDowell +# autodns 1.0.0 +# Copyright 1999-2006 Project Purple. Written by Jonathan McDowell # See ACKNOWLEDGEMENTS file for full details of contributors. # http://www.earth.li/projectpurple/progs/autodns.html # Released under the GPL. # -# $Id: autodns.pl,v 1.7 2005/04/08 11:45:12 noodles Exp $ -# use strict; +use Date::Parse; use Fcntl qw(:flock); +use File::Path; use File::Temp qw(tempfile); use IPC::Open3; use MIME::Parser; -$ENV{'PATH'}="/usr/local/bin:/usr/bin:/bin:/usr/sbin"; +$ENV{'PATH'} = "/usr/local/bin:/usr/bin:/bin:/usr/sbin"; + +my ($from, $subject, $gpguser, $gpggood, $priv); +my ($user, $server, $inprocess, $delcount, $addcount); +my ($domain, @MAIL, @GPGERROR, @COMMANDS, %zones, $VERSION); -my ($from, $subject, $gpguser, $gpggood, $usersfile, $lockfile, $priv); -my ($user, $server, $inprocess, $delcount, $addcount, $reload_command); -my ($domain, @MAIL, @GPGERROR, @COMMANDS, %zones); -my ($me, $ccreply, $conffile, $domainlistroot, @cfgfiles, $VERSION); +use vars qw($me $ccreply $conffile $domainlistroot @cfgfiles $usersfile + $lockfile $reload_command $expiry $zonefiledir); -$VERSION="0.0.7"; +$VERSION = "1.0.0"; # # Load our config @@ -45,13 +47,13 @@ unless (my $ret = do $file) { # # Call with the name of a config file to read: # -# &getzones("/etc/named.conf"); +# getzones("/etc/named.conf"); # -sub getzones { - my ($namedfile) = @_; +sub getzones($) { + my $namedfile = shift; open (NAMEDCONF, "< $namedfile") or - &fatalerror("Can't open $namedfile"); + fatalerror("Can't open $namedfile"); while () { if (/^\s*zone\s*"([^"]+)"/) { @@ -67,10 +69,11 @@ sub getzones { # # These are: a-z, 0-9, - or . # -sub valid_domain { +sub valid_domain($) { my $domain = shift; $domain = lc $domain; - if ($domain =~ /^(?:[a-z0-9-]+\.)+[a-z]{2,4}$/) { + + if ($domain =~ /^(?:[a-z0-9-]+\.)+[a-z]{2,6}$/) { return 1; } elsif ($domain =~ /^(?:[0-9\/-]+\.)+in-addr.arpa$/) { return 1; @@ -85,7 +88,7 @@ sub valid_domain { # # fatalerror("I'm melting!"); # -sub fatalerror { +sub fatalerror($) { my $message = shift; print REPLY $message; @@ -95,7 +98,6 @@ sub fatalerror { close(LOCKFILE); unlink($lockfile); -# die $message; exit; } @@ -104,37 +106,40 @@ sub fatalerror { # # A users entry looks like: # -# ::: +# ::: # # Priviledge level is not currently used. # -# ($user, $priv, $server) = &getuserinfo("5B430367"); +# ($user, $priv, $server) = getuserinfo("5B430367"); # -sub getuserinfo { +sub getuserinfo($) { my $gpguser = shift; - my ($user, $priviledge, $server); + my ($user, $privilege, $server); open (CONFIGFILE, "< $usersfile") or - &fatalerror("Couldn't open user configuration file."); + fatalerror("Couldn't open user configuration file."); foreach () { if (/^([^#.]+):$gpguser:(\d+):(.+)$/) { - $user=$1; - $priviledge=$2; - $server=$3; + $user = $1; + $privilege = $2; + $server = $3; chomp $user; - chomp $priviledge; + chomp $privilege; chomp $server; if ($user !~ /^.+$/) { close(CONFIGFILE); - &fatalerror("Error in user configuration file: Can't get username.\n"); + fatalerror("Error in user configuration ". + "file: Can't get username.\n"); } if ($server !~ /^(\d{1,3}\.){3}\d{1,3}$/) { $server =~ s/\d\.]//g; close(CONFIGFILE); - &fatalerror("Error in user configuration file: Invalid primary server IP address ($server)\n"); + fatalerror("Error in user configuration ". + "file: Invalid primary server IP ". + "address ($server)\n"); exit; } } @@ -142,23 +147,52 @@ sub getuserinfo { close(CONFIGFILE); if ($user =~ /^$/) { - &fatalerror("User not found.\n"); + fatalerror("User not found.\n"); } - return ($user, $priviledge, $server); + return ($user, $privilege, $server); } -$delcount=$addcount=$inprocess=0; +# +# Add a new AutoDNS user. +# +# addautodnsuser($username, $keyid, $priv, $masterip); +# ::: +# +sub addautodnsuser($$$$) { + my $username = shift; + my $keyid = shift; + my $priv = shift; + my $masterip = shift; + + # Create domains file for the user. + open (DOMAINLIST, ">>$domainlistroot$username") or + fatalerror("Couldn't create domains file.\n"); + close DOMAINLIST; + + # Make the directory for the zone files. + my @dirs = mkpath("$zonefiledir/$username", 0, 0775); + fatalerror("Couldn't create zone file directory.\n") + if scalar(@dirs) == 0; + + # Actually add them to the users file. + open(USERFILE, ">> $usersfile") or + fatalerror("Couldn't open user configuration file."); + print USERFILE "$username:$keyid:$priv:$masterip\n"; + close(USERFILE); +} + +$delcount = $addcount = $inprocess = 0; # Read in the mail from stdin. -@MAIL=<>; +@MAIL = <>; $subject = "Reply from AutoDNS"; # Now lets try to find out who it's from. foreach (@MAIL) { if (/^$/) { last; } - if (/^From: (.*)/i) { $from=$1; chomp $from;} - if (/^Subject:\s+(re:)?(.*)$/i) { $subject="Re: ".$2 if ($2);} + if (/^From: (.*)/i) { $from = $1; chomp $from;} + if (/^Subject:\s+(re:)?(.*)$/i) { $subject = "Re: ".$2 if ($2);} } if ((! defined($from)) || $from =~ /^$/ ) { @@ -167,7 +201,7 @@ if ((! defined($from)) || $from =~ /^$/ ) { die "From address is mailer-daemon, ignoring."; } -if (! defined($subject)) { $subject="Reply from AutoDNS"; }; +if (! defined($subject)) { $subject = "Reply from AutoDNS"; }; # We've got a from address. Start a reply. @@ -186,7 +220,7 @@ print REPLY <parse_data(\@MAIL); # # Make sure locale is set to C so we get messages in English as we expect. # -$ENV{'LC_ALL'}="C"; +$ENV{'LC_ALL'} = "C"; if ($entity->parts) { # MIME my ($got_sig, $got_text) = (0, 0); - my ($sig_name,$sig_fh,$text_name,$text_fh); - ($sig_fh, $sig_name) = tempfile(); - ($text_fh, $text_name) = tempfile(); + my ($sig_fh, $sig_name) = tempfile(); + my ($text_fh, $text_name) = tempfile(); foreach my $subent ($entity->parts) { if ($subent->effective_type eq "text/plain") { @@ -226,19 +259,38 @@ if ($entity->parts) { print $sig_fh $subent->as_string; close($sig_fh); $got_sig++; + } elsif ($subent->effective_type eq "multipart/mixed") { + my $str = $subent->as_string; + print $text_fh $str; + close($text_fh); + $got_text++; + + foreach my $mixent ($subent->parts) { + if ($mixent->effective_type eq "text/plain") { + push @COMMANDS, (split /\n/, + $mixent->bodyhandle->as_string); + } + if ($mixent->effective_type eq + "application/pgp-keys") { + push @COMMANDS, (split /\n/, + $mixent->bodyhandle->as_string); + } + } } } if ($got_sig && $got_text) { - open3(\*GPGIN, \*GPGOUT, \*GPGERR, "gpg --batch --verify ". + my $pid = open3(\*GPGIN, \*GPGOUT, \*GPGERR, + "gpg --batch --verify ". $sig_name." ".$text_name); close GPGIN; - @GPGERROR=; - my @GPGOUTPUT=; + @GPGERROR = ; + my @GPGOUTPUT = ; close GPGERR; close GPGOUT; + waitpid $pid, 0; unlink($text_name); unlink($sig_name); @@ -246,25 +298,29 @@ if ($entity->parts) { } else { # Clear text. - open3(\*GPGIN, \*GPGOUT, \*GPGERR, "gpg --batch"); + my $pid = open3(\*GPGIN, \*GPGOUT, \*GPGERR, "gpg --batch --verify"); # Feed it the mail. - print GPGIN @MAIL; + print GPGIN $entity->bodyhandle->as_string; close GPGIN; # And grab what it has to say. - @GPGERROR=; - @COMMANDS=; + @GPGERROR = ; + @COMMANDS = ; + @COMMANDS = split /\n/,$entity->bodyhandle->as_string; close GPGERR; close GPGOUT; + waitpid $pid, 0; } # Check who it's from and if the signature was a good one. $gpggood=1; +my $sigtime = 0; foreach (@GPGERROR) { chomp; - if (/Signature made.* (.*)$/) { - $gpguser=$1; + if (/Signature made (.*) using.*ID (.*)$/) { + $sigtime = str2time($1); + $gpguser = $2; } elsif (/error/) { $gpggood = 0; print REPLY "Some errors ocurred\n"; @@ -293,8 +349,23 @@ if ($gpggood) { exit; } +# Check if the signature is outside our acceptable range. +if (!defined($sigtime)) { + print REPLY "Couldn't parse signature time.\n"; + close REPLY; + exit; +} elsif ($sigtime > (time + $expiry)) { + print REPLY "Signature too far into the future.\n"; + close REPLY; + exit; +} elsif ($sigtime < (time - $expiry)) { + print REPLY "Signature too far into the past.\n"; + close REPLY; + exit; +} + # Now let's check if we know this person. -($user, $priv, $server) = &getuserinfo($gpguser); +($user, $priv, $server) = getuserinfo($gpguser); if (! defined($user) || ! $user) { print REPLY "Unknown user.\n"; @@ -307,8 +378,8 @@ print REPLY "Got user '$user'\n"; # Right. We know this is a valid user. Get a lock to ensure we have exclusive # access to the configs from here on in. open (LOCKFILE,">$lockfile") || - &fatalerror("Couldn't open lock file\n"); -&fatalerror("Couldn't get lock\n") unless(flock(LOCKFILE,LOCK_EX)); + fatalerror("Couldn't open lock file\n"); +fatalerror("Couldn't get lock\n") unless(flock(LOCKFILE,LOCK_EX)); # Ok, now we should figure out what domains we already know about. foreach my $cfgfile (@cfgfiles) { @@ -318,7 +389,7 @@ foreach my $cfgfile (@cfgfiles) { # Force existance of the $domainlistroot$user file if (! -e $domainlistroot.$user) { open (DOMAINLIST, ">>$domainlistroot$user") or - &fatalerror("Couldn't create domains file.\n"); + fatalerror("Couldn't create domains file.\n"); close DOMAINLIST; } @@ -338,9 +409,9 @@ foreach (@COMMANDS) { # Empty line, so ignore it. # } elsif (/^END$/) { - $inprocess=0; + $inprocess = 0; } elsif (/^BEGIN$/) { - $inprocess=1; + $inprocess = 1; } elsif ($inprocess && /^ADD\s+(.*)$/) { $domain = $1; @@ -348,12 +419,13 @@ foreach (@COMMANDS) { $domain =~ tr/[A-Z]/[a-z]/; if (! valid_domain($domain)) { $domain =~ s/[-a-z0-9.]//g; - print REPLY "Invalid character(s) in domain name: $domain\n"; + print REPLY "Invalid character(s) in domain name:", + " $domain\n"; } elsif (defined($zones{$domain}) && $zones{$domain}) { print REPLY "We already secondary $domain\n"; } else { print REPLY "Adding domain $domain\n"; - $zones{$domain}=1; + $zones{$domain} = 1; my $df = $domain; $df =~ tr,/,:,; @@ -372,7 +444,7 @@ zone \"$domain\" { close DOMAINSFILE; open (DOMAINLIST, ">>$domainlistroot$user") or - &fatalerror("Couldn't open file.\n"); + fatalerror("Couldn't open file.\n"); print DOMAINLIST "$domain\n"; close DOMAINLIST; $addcount++; @@ -384,40 +456,48 @@ zone \"$domain\" { $domain =~ tr/[A-Z]/[a-z]/; if (!valid_domain($domain)) { $domain =~ s/[-a-z0-9.]//g; - print REPLY "Invalid character(s) in domain name: $domain\n"; + print REPLY "Invalid character(s) in domain name:", + " $domain\n"; } elsif (!defined($zones{$domain}) || !$zones{$domain}) { print REPLY "$domain does not exist!\n"; } else { print REPLY "Deleting domain $domain\n"; - my (@newcfg,$found); + my (@newcfg, $found); open (DOMAINLIST, "<$domainlistroot$user") or - &fatalerror("Couldn't open file $domainlistroot$user for reading: $!.\n"); + fatalerror("Couldn't open file ". + $domainlistroot.$user. + " for reading: $!.\n"); my @cfg = ; close(DOMAINLIST); @newcfg = grep { ! /^$domain$/ } @cfg; if (scalar @cfg == scalar @newcfg) { - print REPLY "Didn't find $domain in $domainlistroot$user!\n"; - print REPLY "You are only allowed to delete your own domains that exist.\n"; + print REPLY "Didn't find $domain in ", + "$domainlistroot$user!\n"; + print REPLY "You are only allowed to delete", + " your own domains that exist.\n"; next; } open (DOMAINLIST, ">$domainlistroot$user") or - &fatalerror("Couldn't open file $domainlistroot$user for writing: $!.\n"); + fatalerror("Couldn't open file ". + $domainlistroot.$user. + " for writing: $!.\n"); print DOMAINLIST @newcfg; close DOMAINLIST; - $found=0; - @newcfg=(); + $found = 0; + @newcfg = (); open (DOMAINSFILE, "<$conffile") or - &fatalerror("Couldn't open file $conffile for reading: $!\n"); + fatalerror("Couldn't open file $conffile for". + " reading: $!\n"); { local $/ = ''; # eat whole paragraphs while () { unless (/^\s*zone\s+"$domain"/) { push @newcfg, $_; } else { - $found=1; + $found = 1; if ($newcfg[-1] =~ /^###/) { # remove comment and \n pop @newcfg; @@ -427,12 +507,14 @@ zone \"$domain\" { } # end of paragraph eating if (!$found) { - print REPLY "Didn't find $domain in $conffile!\n"; + print REPLY "Didn't find $domain in", + " $conffile!\n"; next; } open (DOMAINSFILE, ">$conffile") or - &fatalerror("Couldn't open $conffile for writing: $!\n"); + fatalerror("Couldn't open $conffile for". + " writing: $!\n"); print DOMAINSFILE @newcfg; close DOMAINSFILE; $delcount++; @@ -453,19 +535,71 @@ zone \"$domain\" { } else { print REPLY "Couldn't open $domainlistroot$user: $!\n"; } + } elsif ($inprocess && /^MASTER\s(.*)$/) { + if (($priv & 1) != 1) { + print REPLY "You're not authorised to use the MASTER ", + "command.\n"; + } elsif ($1 =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/) { + $server = $1; + print REPLY "Set master IP address to $1\n"; + } else { + print REPLY "$1 doesn't look like a valid IPv4 ", + "address to me.\n"; + } + } elsif ($inprocess && /^ADDUSER\s(.*)$/) { + if (($priv & 2) != 2) { + print REPLY "You're not authorised to use the ", + "ADDUSER command.\n"; + } elsif ($1 =~ /^([a-z0-9]+) ([A-Fa-f0-9]{8}) (\d+) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/) { + addautodnsuser($1, $2, $3, $4); + + print REPLY "Attempting to import new key:\n"; + + # Feed our command mail to GPG so we can pull the + # (hopefully included) new GPG key out from it. + my $pid = open3(\*GPGIN, \*GPGOUT, \*GPGERR, + "gpg --batch --import"); + + # Feed it the mail. + print GPGIN join("\n", @COMMANDS); + close GPGIN; + + # And grab what it has to say. + @GPGERROR = ; + my @GPGOUTPUT = ; + close GPGERR; + close GPGOUT; + waitpid $pid, 0; + + print REPLY @GPGERROR; + } else { + print REPLY "ADDUSER parameter error.\n"; + } } elsif ($inprocess && /^HELP$/) { - print REPLY "In order to use the service, you will need to send GPG signed\n"; - print REPLY "messages.\n\n"; - print REPLY "The format of the text in these messages is important, as they represent\n"; - print REPLY "commands to autodns. Commands are formatted one per line, and enclosed\n"; - print REPLY "by \"BEGIN\" and \"END\" commands (without the quotes).\n"; - print REPLY "Current valid commands are:\n"; - print REPLY "BEGIN - begin processing.\n"; - print REPLY "END - end processing.\n"; - print REPLY "HELP - display this message.\n"; - print REPLY "LIST - show all the zones currently held by you.\n"; - print REPLY "ADD - adds the domain for processing.\n"; - print REPLY "DEL - removes the domain if you own it.\n"; + print REPLY < - adds the domain for processing. +DEL - removes the domain if you own it. +EOF + if (($priv & 1) == 1) { + print REPLY "MASTER - set the nameserver". + " we should slave off for subsequent ADD\ncommands.\n"; + } + if (($priv & 2) == 2) { + print REPLY "ADDUSER ", + " - add a new user. Imports any key", + "\nattached to the message into the keyring.\n"; + } } elsif ($inprocess) { print REPLY "Unknown command!\n"; }