Ignore:
Timestamp:
12/10/13 17:15:56 (10 years ago)
Author:
Kris Deugau
Message:

/branches/stable

Merge reverse DNS work; 1 of mumble

  • from branch creation through r261

Minor conflicts in dns.cgi and DNSDB.pm

Location:
branches/stable
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/stable

  • branches/stable/DNSDB.pm

    r438 r544  
    2828use Crypt::PasswdMD5;
    2929use Net::SMTP;
    30 use NetAddr::IP;
     30use NetAddr::IP qw(:lower);
    3131use POSIX;
    3232use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
     
    3939        &changeGroup
    4040        &loadConfig &connectDB &finish
    41         &addDomain &delDomain &domainName &domainID
     41        &addDomain &delDomain &domainName &revName &domainID &addRDNS
     42        &getZoneCount &getZoneList
    4243        &addGroup &delGroup &getChildren &groupName
    4344        &addUser &updateUser &delUser &userFullName &userStatus &getUserData
    4445        &getSOA &getRecLine &getDomRecs &getRecCount
    4546        &addRec &updateRec &delRec
    46         &getParents
     47        &getTypelist
     48        &parentID
    4749        &isParent
    4850        &domStatus &importAXFR
     
    5961                &changeGroup
    6062                &loadConfig &connectDB &finish
    61                 &addDomain &delDomain &domainName &domainID
     63                &addDomain &delDomain &domainName &revName &domainID &addRDNS
     64                &getZoneCount &getZoneList
    6265                &addGroup &delGroup &getChildren &groupName
    6366                &addUser &updateUser &delUser &userFullName &userStatus &getUserData
    6467                &getSOA &getRecLine &getDomRecs &getRecCount
    6568                &addRec &updateRec &delRec
    66                 &getParents
     69                &getTypelist
     70                &parentID
    6771                &isParent
    6872                &domStatus &importAXFR
     
    139143                perpage         => 15,
    140144        );
     145
     146## (Semi)private variables
     147# Hash of functions for validating record types.  Filled in initGlobals() since
     148# it relies on visibility flags from the rectypes table in the DB
     149my %validators;
     150
     151
     152##
     153## utility functions
     154# _rectable()
     155# Takes default+rdns flags, returns appropriate table name
     156sub _rectable {
     157  my $def = shift;
     158  my $rev = shift;
     159
     160  return 'records' if $def ne 'y';
     161  return 'default_records' if $rev ne 'y';
     162  return 'default_rev_records';
     163} # end _rectable()
     164
     165# _recparent()
     166# Takes default+rdns flags, returns appropriate parent-id column name
     167sub _recparent {
     168  my $def = shift;
     169  my $rev = shift;
     170
     171  return 'group_id' if $def eq 'y';
     172  return 'rdns_id' if $rev eq 'y';
     173  return 'domain_id';
     174} # end _recparent()
     175
     176# Check an IP to be added in a reverse zone to see if it's really in the requested parent.
     177# Takes a database handle, default and reverse flags, IP (fragment) to check, parent zone ID,
     178# and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for
     179# database insertion)
     180sub _ipparent {
     181  my $dbh = shift;
     182  my $defrec = shift;
     183  my $revrec = shift;
     184  my $val = shift;
     185  my $id = shift;
     186  my $addr = shift;
     187
     188  return if $revrec ne 'y';     # this sub not useful in forward zones
     189
     190  $$addr = NetAddr::IP->new($$val);      #necessary?
     191
     192  # subsub to split, reverse, and overlay an IP fragment on a netblock
     193  sub __rev_overlay {
     194    my $splitme = shift;        # ':' or '.', m'lud?
     195    my $parnet = shift;
     196    my $val = shift;
     197    my $addr = shift;
     198
     199    my $joinme = $splitme;
     200    $splitme = '\.' if $splitme eq '.';
     201    my @working = reverse(split($splitme, $parnet->addr));
     202    my @parts = reverse(split($splitme, $$val));
     203    for (my $i = 0; $i <= $#parts; $i++) {
     204      $working[$i] = $parts[$i];
     205    }
     206    my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0;
     207    return 0 unless $checkme->within($parnet);
     208    $$addr = $checkme;  # force "correct" IP to be recorded.
     209    return 1;
     210  }
     211
     212  my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id));
     213  my $parnet = NetAddr::IP->new($parstr);
     214
     215  # Fail early on v6-in-v4 or v4-in-v6.  We're not accepting these ATM.
     216  return 0 if $parnet->addr =~ /\./ && $$val =~ /:/;
     217  return 0 if $parnet->addr =~ /:/ && $$val =~ /\./;
     218
     219  if ($$addr && $$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/) {
     220    # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address.
     221    # the rest we have to restructure before fiddling.  *sigh*
     222    return 1 if $$addr->within($parnet);
     223  } else {
     224    # We don't have a complete IP in $$val (yet)
     225    if ($parnet->addr =~ /:/) {
     226      $$val =~ s/^:+//;  # gotta strip'em all...
     227      return __rev_overlay(':', $parnet, $val, $addr);
     228    }
     229    if ($parnet->addr =~ /\./) {
     230      $$val =~ s/^\.+//;
     231      return __rev_overlay('.', $parnet, $val, $addr);
     232    }
     233    # should be impossible to get here...
     234  }
     235  # ... and here.
     236  # can't do nuttin' in forward zones
     237} # end _ipparent()
     238
     239# A little different than _ipparent above;  this tries to *find* the parent zone of a hostname
     240sub _hostparent {
     241  my $dbh = shift;
     242  my $hname = shift;
     243 
     244  my @hostbits = split /\./, $hname;
     245  my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE domain = ? GROUP BY domain_id");
     246  foreach (@hostbits) {
     247    $sth->execute($hname);
     248    my ($found, $parid) = $sth->fetchrow_array;
     249    if ($found) {
     250      return $parid;
     251    }
     252    $hname =~ s/^$_\.//;
     253  }
     254} # end _hostparent()
     255
     256##
     257## Record validation subs.
     258##
     259
     260# A record
     261sub _validate_1 {
     262  my $dbh = shift;
     263
     264  my %args = @_;
     265
     266  return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y';
     267
     268  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     269  # or the intended parent domain for live records.
     270  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
     271  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     272
     273  # Check IP is well-formed, and that it's a v4 address
     274  # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
     275  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
     276        unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/;
     277  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
     278        unless $args{addr} && !$args{addr}->{isv6};
     279  # coerce IP/value to normalized form for storage
     280  ${$args{val}} = $args{addr}->addr;
     281
     282  return ('OK','OK');
     283} # done A record
     284
     285# NS record
     286sub _validate_2 {
     287  my $dbh = shift;
     288
     289  my %args = @_;
     290
     291  # Coerce the hostname to "DOMAIN" for forward default records, "ZONE" for reverse default records,
     292  # or the intended parent zone for live records.
     293##fixme:  allow for delegating <subdomain>.DOMAIN?
     294  if ($args{revrec} eq 'y') {
     295    my $pname = ($args{defrec} eq 'y' ? 'ZONE' : revName($dbh,$args{id}));
     296    ${$args{host}} = $pname if ${$args{host}} ne $pname;
     297  } else {
     298    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
     299    ${$args{host}} = $pname if ${$args{host}} ne $pname;
     300  }
     301
     302# Let this lie for now.  Needs more magic.
     303#  # Check IP is well-formed, and that it's a v4 address
     304#  return ('FAIL',"A record must be a valid IPv4 address")
     305#       unless $addr && !$addr->{isv6};
     306#  # coerce IP/value to normalized form for storage
     307#  $$val = $addr->addr;
     308
     309  return ('OK','OK');
     310} # done NS record
     311
     312# CNAME record
     313sub _validate_5 {
     314  my $dbh = shift;
     315
     316  my %args = @_;
     317
     318# Not really true, but these are only useful for delegating smaller-than-/24 IP blocks.
     319# This is fundamentally a messy operation and should really just be taken care of by the
     320# export process, not manual maintenance of the necessary records.
     321  return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y';
     322
     323  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     324  # or the intended parent domain for live records.
     325  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
     326  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     327
     328  return ('OK','OK');
     329} # done CNAME record
     330
     331# SOA record
     332sub _validate_6 {
     333  # Smart monkeys won't stick their fingers in here;  we have
     334  # separate dedicated routines to deal with SOA records.
     335  return ('OK','OK');
     336} # done SOA record
     337
     338# PTR record
     339sub _validate_12 {
     340  my $dbh = shift;
     341
     342  my %args = @_;
     343
     344  if ($args{revrec} eq 'y') {
     345    if ($args{defrec} eq 'n') {
     346      return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id}))
     347        unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
     348      ${$args{val}} = $args{addr}->addr;
     349    } else {
     350      if (${$args{val}} =~ /\./) {
     351        # looks like a v4 or fragment
     352        if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) {
     353          # woo!  a complete IP!  validate it and normalize, or fail.
     354          $args{addr} = NetAddr::IP->new(${$args{val}})
     355                or return ('FAIL', "IP/value looks like IPv4 but isn't valid");
     356          ${$args{val}} = $args{addr}->addr;
     357        } else {
     358          ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/;
     359        }
     360      } elsif (${$args{val}} =~ /[a-f:]/) {
     361        # looks like a v6 or fragment
     362        ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/;
     363        if ($args{addr}) {
     364          if ($args{addr}->addr =~ /^0/) {
     365            ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/;
     366          } else {
     367            ${$args{val}} = $args{addr}->addr;
     368          }
     369        }
     370      } else {
     371        # bare number (probably).  These could be v4 or v6, so we'll
     372        # expand on these on creation of a reverse zone.
     373        ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/;
     374      }
     375      ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/;
     376    }
     377
     378# Multiple PTR records do NOT generally do what most people believe they do,
     379# and tend to fail in the most awkward way possible.  Check and warn.
     380# We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12"
     381
     382    my @checkvals = (${$args{val}});
     383    if (${$args{val}} =~ /,/) {
     384      # push . and :: variants into checkvals if val has ,
     385      my $tmp;
     386      ($tmp = ${$args{val}}) =~ s/,/./;
     387      push @checkvals, $tmp;
     388      ($tmp = ${$args{val}}) =~ s/,/::/;
     389      push @checkvals, $tmp;
     390    }
     391    my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?");
     392    foreach my $checkme (@checkvals) {
     393      my $ptrcount;
     394      ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
     395        " WHERE val = ?", undef, ($checkme));
     396      return ('WARN', "PTR record for $checkme already exists;  adding another will probably not do what you want")
     397        if $ptrcount;
     398    }
     399  } else {
     400    # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations
     401    # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct
     402    # PTR records on export
     403    return ('FAIL',"Forward zones cannot contain PTR records");
     404  }
     405
     406  return ('OK','OK');
     407} # done PTR record
     408
     409# MX record
     410sub _validate_15 {
     411  my $dbh = shift;
     412
     413  my %args = @_;
     414
     415# Not absolutely true but WTF use is an MX record for a reverse zone?
     416  return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y';
     417
     418  return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}});
     419  ${$args{dist}} =~ s/\s*//g;
     420  return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
     421
     422  ${$args{fields}} = "distance,";
     423  push @{$args{vallist}}, ${$args{dist}};
     424
     425  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     426  # or the intended parent domain for live records.
     427  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
     428  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     429
     430  return ('OK','OK');
     431} # done MX record
     432
     433# TXT record
     434sub _validate_16 {
     435  # Could arguably put a WARN return here on very long (>512) records
     436  return ('OK','OK');
     437} # done TXT record
     438
     439# RP record
     440sub _validate_17 {
     441  # Probably have to validate these some day
     442  return ('OK','OK');
     443} # done RP record
     444
     445# AAAA record
     446sub _validate_28 {
     447  my $dbh = shift;
     448
     449  my %args = @_;
     450
     451  return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y';
     452
     453  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     454  # or the intended parent domain for live records.
     455  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
     456  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     457
     458  # Check IP is well-formed, and that it's a v6 address
     459  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address")
     460        unless $args{addr} && $args{addr}->{isv6};
     461  # coerce IP/value to normalized form for storage
     462  ${$args{val}} = $args{addr}->addr;
     463
     464  return ('OK','OK');
     465} # done AAAA record
     466
     467# SRV record
     468sub _validate_33 {
     469  my $dbh = shift;
     470
     471  my %args = @_;
     472
     473# Not absolutely true but WTF use is an SRV record for a reverse zone?
     474  return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y';
     475
     476  return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}});
     477  ${$args{dist}} =~ s/\s*//g;
     478  return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
     479
     480  return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
     481        unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
     482  return ('FAIL',"Port and weight are required for SRV records")
     483        unless defined(${$args{weight}}) && defined(${$args{port}});
     484  ${$args{weight}} =~ s/\s*//g;
     485  ${$args{port}} =~ s/\s*//g;
     486
     487  return ('FAIL',"Port and weight are required, and must be numeric")
     488        unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/;
     489
     490  ${$args{fields}} = "distance,weight,port,";
     491  push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}});
     492
     493  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     494  # or the intended parent domain for live records.
     495  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id}));
     496  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     497
     498  return ('OK','OK');
     499} # done SRV record
     500
     501# Now the custom types
     502
     503# A+PTR record.  With a very little bit of magic we can also use this sub to validate AAAA+PTR.  Whee!
     504sub _validate_65280 {
     505  my $dbh = shift;
     506
     507  my %args = @_;
     508
     509  my $code = 'OK';
     510  my $msg = 'OK';
     511
     512  if ($args{defrec} eq 'n') {
     513    # live record;  revrec determines whether we validate the PTR or A component first.
     514
     515    if ($args{revrec} eq 'y') {
     516      ($code,$msg) = _validate_12($dbh, %args);
     517      return ($code,$msg) if $code eq 'FAIL';
     518
     519      # Check if the reqested domain exists.  If not, coerce the type down to PTR and warn.
     520      if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) {
     521        my $addmsg = "Record added as PTR instead of $typemap{${$args{rectype}}};  domain not found for ${$args{host}}";
     522        $msg .= "\n$addmsg" if $code eq 'WARN';
     523        $msg = $addmsg if $code eq 'OK';
     524        ${$args{rectype}} = $reverse_typemap{PTR};
     525        return ('WARN', $msg);
     526      }
     527
     528      # Add domain ID to field list and values
     529      ${$args{fields}} .= "domain_id,";
     530      push @{$args{vallist}}, ${$args{domid}};
     531
     532    } else {
     533      ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280;
     534      ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281;
     535      return ($code,$msg) if $code eq 'FAIL';
     536
     537      # Check if the requested reverse zone exists - note, an IP fragment won't
     538      # work here since we don't *know* which parent to put it in.
     539      # ${$args{val}} has been validated as a valid IP by now, in one of the above calls.
     540      my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?".
     541        " ORDER BY masklen(revnet) DESC", undef, (${$args{val}}));
     542      if (!$revid) {
     543        $msg = "Record added as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA').
     544                " instead of $typemap{${$args{rectype}}};  reverse zone not found for ${$args{val}}";
     545        ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA});
     546        return ('WARN', $msg);
     547      }
     548
     549      # Check for duplicate PTRs.  Note we don't have to play games with $code and $msg, because
     550      # by definition there can't be duplicate PTRs if the reverse zone isn't managed here.
     551      my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
     552        " WHERE val = ?", undef, ${$args{val}});
     553      if ($ptrcount) {
     554        $msg = "PTR record for ${$args{val}} already exists;  adding another will probably not do what you want";
     555        $code = 'WARN';
     556      }
     557
     558      ${$args{fields}} .= "rdns_id,";
     559      push @{$args{vallist}}, $revid;
     560    }
     561
     562  } else {      # defrec eq 'y'
     563    if ($args{revrec} eq 'y') {
     564      ($code,$msg) = _validate_12($dbh, %args);
     565      return ($code,$msg) if $code eq 'FAIL';
     566      if (${$args{rectype}} == 65280) {
     567        return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment")
     568                if ${$args{val}} =~ /:/;
     569        ${$args{val}} =~ s/^ZONE,/ZONE./;       # Clean up after uncertain IP-fragment-type from _validate_12
     570      } elsif (${$args{rectype}} == 65281) {
     571        return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment")
     572                if ${$args{val}} =~ /\./;
     573        ${$args{val}} =~ s/^ZONE,/ZONE::/;      # Clean up after uncertain IP-fragment-type from _validate_12
     574      }
     575    } else {
     576      # This is easy.  I also can't see a real use-case for A/AAAA+PTR in *all* forward
     577      # domains, since you wouldn't be able to substitute both domain and reverse zone
     578      # sanely, and you'd end up with guaranteed over-replicated PTR records that would
     579      # confuse the hell out of pretty much anything that uses them.
     580##fixme: make this a config flag?
     581      return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains");
     582    }
     583  }
     584
     585  return ($code, $msg);
     586} # done A+PTR record
     587
     588# AAAA+PTR record
     589# A+PTR above has been magicked to handle AAAA+PTR as well.
     590sub _validate_65281 {
     591  return _validate_65280(@_);
     592} # done AAAA+PTR record
     593
     594# PTR template record
     595sub _validate_65282 {
     596  return ('OK','OK');
     597} # done PTR template record
     598
     599# A+PTR template record
     600sub _validate_65283 {
     601  return ('OK','OK');
     602} # done AAAA+PTR template record
     603
     604# AAAA+PTR template record
     605sub _validate_65284 {
     606  return ('OK','OK');
     607} # done AAAA+PTR template record
     608
    141609
    142610
     
    327795
    328796# load record types from database
    329   my $sth = $dbh->prepare("select val,name from rectypes");
     797  my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes");
    330798  $sth->execute;
    331   while (my ($recval,$recname) = $sth->fetchrow_array()) {
     799  while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) {
    332800    $typemap{$recval} = $recname;
    333801    $reverse_typemap{$recname} = $recval;
     802    # now we fill the record validation function hash
     803    if ($stdflag < 5) {
     804      my $fn = "_validate_$recval";
     805      $validators{$recval} = \&$fn;
     806    } else {
     807      my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }";
     808      $validators{$recval} = eval $fn;
     809    }
    334810  }
    335811} # end initGlobals
     
    5341010# Log an action
    5351011# Internal sub
    536 # Takes a database handle, domain_id, user_id, group_id, email, name and log entry
     1012# Takes a database handle and log entry hash containing at least:
     1013# user_id, group_id, log entry
     1014# and optionally one or more of:
     1015# username/email, user full name, domain_id, rdns_id
    5371016##fixme:  convert to trailing hash for user info
    5381017# User info must contain a (user ID OR username)+fullname
    5391018sub _log {
    5401019  my $dbh = shift;
    541   my ($domain_id,$user_id,$group_id,$username,$name,$entry) = @_;
     1020
     1021  my %args = @_;
     1022
     1023  $args{rdns_id} = 0 if !$args{rdns_id};
     1024  $args{domain_id} = 0 if !$args{domain_id};
    5421025
    5431026##fixme:  need better way(s?) to snag userinfo for log entries.  don't want to have
    5441027# to pass around yet *another* constant (already passing $dbh, shouldn't need to)
    5451028  my $fullname;
    546   if (!$user_id) {
    547     ($user_id, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users".
    548         " WHERE username=?", undef, ($username));
    549   } elsif (!$username) {
    550     ($username, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users".
    551         " WHERE user_id=?", undef, ($user_id));
    552   } else {
     1029  if (!$args{user_id}) {
     1030    ($args{user_id}, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users".
     1031        " WHERE username=?", undef, ($args{username}));
     1032  }
     1033  if (!$args{username}) {
     1034    ($args{username}, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users".
     1035        " WHERE user_id=?", undef, ($args{user_id}));
     1036  }
     1037  if (!$args{fullname}) {
    5531038    ($fullname) = $dbh->selectrow_array("SELECT firstname || ' ' || lastname FROM users".
    554         " WHERE user_id=?", undef, ($user_id));
    555   }
    556 
    557   $name = $fullname if !$name;
     1039        " WHERE user_id=?", undef, ($args{user_id}));
     1040  }
     1041
     1042  $args{name} = $fullname if !$args{name};
    5581043
    5591044##fixme:  farm out the actual logging to different subs for file, syslog, internal, etc based on config
    560   $dbh->do("INSERT INTO log (domain_id,user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?)", undef,
    561         ($domain_id,$user_id,$group_id,$username,$name,$entry));
    562 #            123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
    563 #                     1         2         3         4         5         6         7
     1045  $dbh->do("INSERT INTO log (domain_id,rdns_id,user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?,?)",
     1046        undef,
     1047        ($args{domain_id},$args{rdns_id},$args{user_id},$args{group_id},$args{username},$args{name},$args{entry}));
     1048
    5641049} # end _log
    5651050
     
    6191104    ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain));
    6201105
    621     _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},
    622         "Added ".($state ? 'active' : 'inactive')." domain $domain");
     1106    _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{username},
     1107        entry => "Added ".($state ? 'active' : 'inactive')." domain $domain"));
    6231108
    6241109    # ... and now we construct the standard records from the default set.  NB:  group should be variable.
     
    6341119        my @tmp1 = split /:/, $host;
    6351120        my @tmp2 = split /:/, $val;
    636         _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},
     1121        _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group,
     1122                username => $userinfo{username}, entry =>
    6371123                "[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
    638                 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl");
     1124                "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl"));
    6391125      } else {
    6401126        my $logentry = "[new $domain] Added record '$host $typemap{$type}";
    6411127        $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX';
    6421128        $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV';
    643         _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},
    644                 $logentry." $val', TTL $ttl");
     1129        _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group,
     1130                username => $userinfo{username}, entry =>
     1131                $logentry." $val', TTL $ttl"));
    6451132      }
    6461133    }
     
    7131200
    7141201
     1202## DNSDB::revName()
     1203# Return the reverse zone name based on an rDNS ID
     1204# Takes a database handle and the rDNS ID
     1205# Returns the reverse zone name or undef on failure
     1206sub revName {
     1207  $errstr = '';
     1208  my $dbh = shift;
     1209  my $revid = shift;
     1210  my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) );
     1211  $errstr = $DBI::errstr if !$revname;
     1212  return $revname if $revname;
     1213} # end revName()
     1214
     1215
    7151216## DNSDB::domainID()
    7161217# Takes a database handle and domain name
     
    7241225  return $domid if $domid;
    7251226} # end domainID()
     1227
     1228
     1229## DNSDB::addRDNS
     1230# Adds a reverse DNS zone
     1231# Takes a database handle, CIDR block, numeric group, boolean(ish) state (active/inactive),
     1232# and user info hash (for logging).
     1233# Returns a status code and message
     1234sub addRDNS {
     1235  my $dbh = shift;
     1236  my $zone = NetAddr::IP->new(shift);
     1237  return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/);
     1238  my $revpatt = shift;
     1239  my $group = shift;
     1240  my $state = shift;
     1241
     1242  my %userinfo = @_;    # remaining bits.
     1243# user ID, username, user full name
     1244
     1245  $state = 1 if $state =~ /^active$/;
     1246  $state = 1 if $state =~ /^on$/;
     1247  $state = 0 if $state =~ /^inactive$/;
     1248  $state = 0 if $state =~ /^off$/;
     1249
     1250  return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/;
     1251
     1252# quick check to start to see if we've already got one
     1253  my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revzone=?", undef, ("$zone"));
     1254
     1255  return ('FAIL', "Zone already exists") if $rdns_id;
     1256
     1257  # Allow transactions, and raise an exception on errors so we can catch it later.
     1258  # Use local to make sure these get "reset" properly on exiting this block
     1259  local $dbh->{AutoCommit} = 0;
     1260  local $dbh->{RaiseError} = 1;
     1261
     1262#$dbh->selectrow_array("SELECT currval('users_user_id_seq')");
     1263  # Wrap all the SQL in a transaction
     1264  eval {
     1265    # insert the domain...
     1266    $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($zone, $group, $state));
     1267
     1268    # get the ID...
     1269    ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
     1270
     1271    _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{name},
     1272        entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone"));
     1273
     1274    # ... and now we construct the standard records from the default set.  NB:  group should be variable.
     1275    my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?");
     1276    my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,host,type,val,ttl)".
     1277        " VALUES ($rdns_id,?,?,?,?)");
     1278    $sth->execute($group);
     1279    while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) {
     1280      $host =~ s/ADMINDOMAIN/$config{domain}/g;
     1281##work
     1282# - replace ZONE in $val
     1283# - skip records not appropriate for the zone (skip A+PTR on v6 zones, and AAAA+PTR on v4 zones)
     1284#      $val =~ s/DOMAIN/$domain/g;
     1285      $sth_in->execute($host,$type,$val,$ttl);
     1286      if ($typemap{$type} eq 'SOA') {
     1287        my @tmp1 = split /:/, $host;
     1288        my @tmp2 = split /:/, $val;
     1289        _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group,
     1290                username => $userinfo{name}, entry =>
     1291                "[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
     1292                "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl"));
     1293      } else {
     1294        my $logentry = "[new $zone] Added record '$host $typemap{$type}";
     1295#       $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX';
     1296#       $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV';
     1297        _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group,
     1298                username => $userinfo{name}, entry =>
     1299                $logentry." $val', TTL $ttl"));
     1300      }
     1301    }
     1302
     1303    # once we get here, we should have suceeded.
     1304    $dbh->commit;
     1305  }; # end eval
     1306
     1307  if ($@) {
     1308    my $msg = $@;
     1309    eval { $dbh->rollback; };
     1310    return ('FAIL',$msg);
     1311  } else {
     1312    return ('OK',$rdns_id);
     1313  }
     1314
     1315} # end addRDNS()
     1316
     1317
     1318## DNSDB::getZoneCount
     1319# Get count of zones in group or groups
     1320# Takes a database handle and hash containing:
     1321#  - the "current" group
     1322#  - an array of "acceptable" groups
     1323#  - a flag for forward/reverse zones
     1324#  - Optionally accept a "starts with" and/or "contains" filter argument
     1325# Returns an integer count of the resulting zone list.
     1326sub getZoneCount {
     1327  my $dbh = shift;
     1328
     1329  my %args = @_;
     1330
     1331  my @filterargs;
     1332  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
     1333  push @filterargs, "^$args{startwith}" if $args{startwith};
     1334  $args{filter} =~ s/\./\[\.\]/g if $args{filter};      # only match literal dots, usually in reverse zones
     1335  push @filterargs, $args{filter} if $args{filter};
     1336
     1337  my $sql;
     1338  # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read
     1339  if ($args{revrec} eq 'n') {
     1340    $sql = "SELECT count(*) FROM domains".
     1341        " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
     1342        ($args{startwith} ? " AND domain ~* ?" : '').
     1343        ($args{filter} ? " AND domain ~* ?" : '');
     1344  } else {
     1345    $sql = "SELECT count(*) FROM revzones".
     1346        " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
     1347        ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '').
     1348        ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '');
     1349  }
     1350  my ($count) = $dbh->selectrow_array($sql, undef, @filterargs);
     1351  return $count;
     1352} # end getZoneCount()
     1353
     1354
     1355## DNSDB::getZoneList()
     1356# Get a list of zones in the specified group(s)
     1357# Takes the same arguments as getZoneCount() above
     1358# Returns a reference to an array of hashrefs suitable for feeding to HTML::Template
     1359sub getZoneList {
     1360  my $dbh = shift;
     1361
     1362  my %args = @_;
     1363
     1364  my @zonelist;
     1365
     1366  $args{sortorder} = 'ASC' if !grep $args{sortorder}, ('ASC','DESC');
     1367  $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
     1368
     1369  my @filterargs;
     1370  $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
     1371  push @filterargs, "^$args{startwith}" if $args{startwith};
     1372  $args{filter} =~ s/\./\[\.\]/g if $args{filter};      # only match literal dots, usually in reverse zones
     1373  push @filterargs, $args{filter} if $args{filter};
     1374
     1375  my $sql;
     1376  # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read
     1377  if ($args{revrec} eq 'n') {
     1378    $args{sortby} = 'domain' if !grep $args{sortby}, ('revnet','group','status');
     1379    $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains".
     1380        " INNER JOIN groups ON domains.group_id=groups.group_id".
     1381        " WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
     1382        ($args{startwith} ? " AND domain ~* ?" : '').
     1383        ($args{filter} ? " AND domain ~* ?" : '');
     1384  } else {
     1385##fixme:  arguably startwith here is irrelevant.  depends on the UI though.
     1386    $args{sortby} = 'revnet' if !grep $args{sortby}, ('domain','group','status');
     1387    $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones".
     1388        " INNER JOIN groups ON revzones.group_id=groups.group_id".
     1389        " WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
     1390        ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '').
     1391        ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '');
     1392  }
     1393  # A common tail.
     1394  $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ".
     1395        ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}".
     1396        " OFFSET ".$args{offset}*$config{perpage});
     1397  my $sth = $dbh->prepare($sql);
     1398  $sth->execute(@filterargs);
     1399  my $rownum = 0;
     1400
     1401  while (my @data = $sth->fetchrow_array) {
     1402    my %row;
     1403    $row{domainid} = $data[0];
     1404    $row{domain} = $data[1];
     1405    $row{status} = $data[2];
     1406    $row{group} = $data[3];
     1407    push @zonelist, \%row;
     1408  }
     1409
     1410  return \@zonelist;
     1411} # end getZoneList()
    7261412
    7271413
     
    7621448    $sth->execute($pargroup,$groupname);
    7631449
    764     $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?");
    765     $sth->execute($groupname);
    766     my ($groupid) = $sth->fetchrow_array();
     1450    my ($groupid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname));
    7671451
    7681452# Permissions
     
    7891473
    7901474# Default records
    791     $sth = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
     1475    my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
    7921476        "VALUES ($groupid,?,?,?,?,?,?,?)");
     1477    my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ".
     1478        "VALUES ($groupid,?,?,?,?)");
    7931479    if ($inherit) {
    7941480      # Duplicate records from parent.  Actually relying on inherited records feels
     
    7971483      $sth2->execute($pargroup);
    7981484      while (my @clonedata = $sth2->fetchrow_array) {
    799         $sth->execute(@clonedata);
     1485        $sthf->execute(@clonedata);
     1486      }
     1487      # And now the reverse records
     1488      $sth2 = $dbh->prepare("SELECT group_id,host,type,val,ttl FROM default_rev_records WHERE group_id=?");
     1489      $sth2->execute($pargroup);
     1490      while (my @clonedata = $sth2->fetchrow_array) {
     1491        $sthr->execute(@clonedata);
    8001492      }
    8011493    } else {
     
    8031495      # reasonable basic defaults for SOA, MX, NS, and minimal hosting
    8041496      # could load from a config file, but somewhere along the line we need hardcoded bits.
    805       $sth->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
    806       $sth->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
    807       $sth->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
    808       $sth->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
    809       $sth->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
    810       $sth->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
     1497      $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
     1498      $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
     1499      $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
     1500      $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
     1501      $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
     1502      $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
     1503      # reasonable basic defaults for generic reverse zone.  Same as initial SQL tabledef.
     1504      $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400);
     1505      $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600);
    8111506    }
    8121507
     
    12221917## DNSDB::getSOA()
    12231918# Return all suitable fields from an SOA record in separate elements of a hash
    1224 # Takes a database handle, default/live flag, and group (default) or domain (live) ID
     1919# Takes a database handle, default/live flag, domain/reverse flag, and parent ID
    12251920sub getSOA {
    12261921  $errstr = '';
    12271922  my $dbh = shift;
    12281923  my $def = shift;
     1924  my $rev = shift;
    12291925  my $id = shift;
    12301926  my %ret;
    12311927
    1232   # (ab)use distance and weight columns to store SOA data
    1233 
    1234   my $sql = "SELECT record_id,host,val,ttl,distance from";
    1235   if ($def eq 'def' or $def eq 'y') {
    1236     $sql .= " default_records WHERE group_id=? AND type=$reverse_typemap{SOA}";
    1237   } else {
    1238     # we're editing a live SOA record;  find based on domain
    1239     $sql .= " records WHERE domain_id=? AND type=$reverse_typemap{SOA}";
    1240   }
     1928  # (ab)use distance and weight columns to store SOA data?  can't for default_rev_records...
     1929  # - should really attach serial to the zone parent somewhere
     1930
     1931  my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev).
     1932        " WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}";
     1933
    12411934  my $sth = $dbh->prepare($sql);
    12421935  $sth->execute($id);
    1243 
    1244   my ($recid,$host,$val,$ttl,$serial) = $sth->fetchrow_array() or return;
     1936##fixme:  stick a flag somewhere if the record doesn't exist.  by the API, this is an impossible case, but...
     1937
     1938  my ($recid,$host,$val,$ttl) = $sth->fetchrow_array() or return;
    12451939  my ($contact,$prins) = split /:/, $host;
    12461940  my ($refresh,$retry,$expire,$minttl) = split /:/, $val;
     
    12481942  $ret{recid}   = $recid;
    12491943  $ret{ttl}     = $ttl;
    1250   $ret{serial}  = $serial;
     1944#  $ret{serial} = $serial;      # ca't use distance for serial with default_rev_records
    12511945  $ret{prins}   = $prins;
    12521946  $ret{contact} = $contact;
     
    12601954
    12611955
     1956## DNSDB::updateSOA()
     1957# Update the specified SOA record
     1958# Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash
     1959sub updateSOA {
     1960  my $dbh = shift;
     1961  my $defrec = shift;
     1962  my $revrec = shift;
     1963
     1964  my %soa = @_;
     1965
     1966##fixme: data validation: make sure {recid} is really the SOA for {parent}
     1967  my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6";
     1968  $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}",
     1969        $soa{ttl}, $soa{recid}));
     1970
     1971} # end updateSOA()
     1972
     1973
    12621974## DNSDB::getRecLine()
    12631975# Return all data fields for a zone record in separate elements of a hash
    1264 # Takes a database handle, default/live flag, and record ID
     1976# Takes a database handle, default/live flag, forward/reverse flag, and record ID
    12651977sub getRecLine {
    12661978  $errstr = '';
    12671979  my $dbh = shift;
    1268   my $def = shift;
     1980  my $defrec = shift;
     1981  my $revrec = shift;
    12691982  my $id = shift;
    12701983
    1271   my $sql = "SELECT record_id,host,type,val,distance,weight,port,ttl".
    1272         (($def eq 'def' or $def eq 'y') ? ',group_id FROM default_' : ',domain_id FROM ').
    1273         "records WHERE record_id=?";
     1984  my $sql = "SELECT record_id,host,type,val,ttl".($revrec eq 'n' ? ',distance,weight,port' : '').
     1985        (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM ').
     1986        _rectable($defrec,$revrec)." WHERE record_id=?";
    12741987  my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
    12751988
     
    12841997  }
    12851998
    1286   $ret->{parid} = (($def eq 'def' or $def eq 'y') ? $ret->{group_id} : $ret->{domain_id});
     1999  # explicitly set a parent id
     2000  if ($defrec eq 'y') {
     2001    $ret->{parid} = $ret->{group_id};
     2002  } else {
     2003    $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id});
     2004    # and a secondary if we have a custom type that lives in both a forward and reverse zone
     2005    $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279;
     2006  }
    12872007
    12882008  return $ret;
     
    12992019  $errstr = '';
    13002020  my $dbh = shift;
    1301   my $type = shift;
     2021  my $def = shift;
     2022  my $rev = shift;
    13022023  my $id = shift;
    13032024  my $nrecs = shift || 'all';
     
    13102031  my $filter = shift || '';
    13112032
    1312   $type = 'y' if $type eq 'def';
    1313 
    1314   my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.distance,r.weight,r.port,r.ttl FROM ";
    1315   $sql .= "default_" if $type eq 'y';
    1316   $sql .= "records r ";
     2033  my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl";
     2034  $sql .= ",r.distance,r.weight,r.port" if $rev eq 'n';
     2035  $sql .= " FROM "._rectable($def,$rev)." r ";
    13172036
    13182037  # whee!  multisort means just passing comma-separated fields in sortby!
     
    13262045
    13272046  $sql .= "INNER JOIN rectypes t ON r.type=t.val ";     # for sorting by type alphabetically
    1328   if ($type eq 'y') {
    1329     $sql .= "WHERE r.group_id=?";
    1330   } else {
    1331     $sql .= "WHERE r.domain_id=?";
    1332   }
     2047  $sql .= "WHERE "._recparent($def,$rev)." = ?";
    13332048  $sql .= " AND NOT r.type=$reverse_typemap{SOA}";
    13342049  $sql .= " AND host ~* ?" if $filter;
    13352050  # use alphaorder column for "correct" ordering of sort-by-type instead of DNS RR type number
    13362051  $sql .= " ORDER BY $newsort $direction";
    1337   $sql .= " LIMIT $nrecs OFFSET ".($nstart*$nrecs) if $nstart ne 'all';
    13382052
    13392053  my @bindvars = ($id);
    13402054  push @bindvars, $filter if $filter;
     2055
     2056  # just to be ultraparanoid about SQL injection vectors
     2057  if ($nstart ne 'all') {
     2058    $sql .= " LIMIT ? OFFSET ?";
     2059    push @bindvars, $nrecs;
     2060    push @bindvars, ($nstart*$nrecs);
     2061  }
    13412062  my $sth = $dbh->prepare($sql) or warn $dbh->errstr;
    13422063  $sth->execute(@bindvars) or warn "$sql: ".$sth->errstr;
     
    13532074
    13542075## DNSDB::getRecCount()
    1355 # Return count of non-SOA records in domain (or default records in a group)
    1356 # Takes a database handle, default/live flag, group/domain ID, and optional filtering modifier
     2076# Return count of non-SOA records in zone (or default records in a group)
     2077# Takes a database handle, default/live flag, reverse/forward flag, group/domain ID,
     2078# and optional filtering modifier
    13572079# Returns the count
    13582080sub getRecCount {
    13592081  my $dbh = shift;
    13602082  my $defrec = shift;
     2083  my $revrec = shift;
    13612084  my $id = shift;
    13622085  my $filter = shift || '';
     
    13682091  my @bindvars = ($id);
    13692092  push @bindvars, $filter if $filter;
    1370   my ($count) = $dbh->selectrow_array("SELECT count(*) FROM ".
    1371         ($defrec eq 'y' ? 'default_' : '')."records ".
    1372         "WHERE ".($defrec eq 'y' ? 'group' : 'domain')."_id=? ".
    1373         "AND NOT type=$reverse_typemap{SOA}".
    1374         ($filter ? " AND host ~* ?" : ''),
    1375         undef, (@bindvars) );
     2093  my $sql = "SELECT count(*) FROM ".
     2094        _rectable($defrec,$revrec).
     2095        " WHERE "._recparent($defrec,$revrec)."=? ".
     2096        "AND NOT type=$reverse_typemap{SOA}".
     2097        ($filter ? " AND host ~* ?" : '');
     2098  my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) );
    13762099
    13772100  return $count;
     
    13872110# and weight/port for SRV
    13882111# Returns a status code and detail message in case of error
     2112##fixme:  pass a hash with the record data, not a series of separate values
    13892113sub addRec {
    13902114  $errstr = '';
    13912115  my $dbh = shift;
    13922116  my $defrec = shift;
    1393   my $id = shift;
     2117  my $revrec = shift;
     2118  my $id = shift;       # parent (group_id for defrecs, rdns_id for reverse records,
     2119                        # domain_id for domain records)
    13942120
    13952121  my $host = shift;
    1396   my $rectype = shift;
     2122  my $rectype = shift;  # reference so we can coerce it if "+"-types can't find both zones
    13972123  my $val = shift;
    13982124  my $ttl = shift;
     
    14182144  }
    14192145
     2146  my $domid = 0;
     2147  my $revid = 0;
     2148
     2149  my $retcode = 'OK';   # assume everything will go OK
     2150  my $retmsg = '';
     2151
     2152  # do simple validation first
    14202153  return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/;
    14212154
    1422   my $fields = ($defrec eq 'y' ? 'group_id' : 'domain_id').",host,type,val,ttl";
    1423   my $vallen = "?,?,?,?,?";
    1424   my @vallist = ($id,$host,$rectype,$val,$ttl);
    1425 
    1426   my $dist;
    1427   if ($rectype == $reverse_typemap{MX} or $rectype == $reverse_typemap{SRV}) {
    1428     $dist = shift;
    1429     return ('FAIL',"Distance is required for $typemap{$rectype} records") unless defined($dist);
    1430     $dist =~ s/\s*//g;
    1431     return ('FAIL',"Distance is required, and must be numeric") unless $dist =~ /^\d+$/;
    1432     $fields .= ",distance";
    1433     $vallen .= ",?";
    1434     push @vallist, $dist;
    1435   }
    1436   my $weight;
    1437   my $port;
    1438   if ($rectype == $reverse_typemap{SRV}) {
    1439     # check for _service._protocol.  NB:  RFC2782 does not say "MUST"...  nor "SHOULD"...
    1440     # it just says (paraphrased) "... is prepended with _ to prevent DNS collisions"
    1441     return ('FAIL',"SRV records must begin with _service._protocol [$host]")
    1442         unless $host =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
    1443     $weight = shift;
    1444     $port = shift;
    1445     return ('FAIL',"Port and weight are required for SRV records") unless defined($weight) && defined($port);
    1446     $weight =~ s/\s*//g;
    1447     $port =~ s/\s*//g;
    1448     return ('FAIL',"Port and weight are required, and must be numeric")
    1449         unless $weight =~ /^\d+$/ && $port =~ /^\d+$/;
    1450     $fields .= ",weight,port";
    1451     $vallen .= ",?,?";
    1452     push @vallist, ($weight,$port);
    1453   }
     2155  # Quick check on hostname parts.  Note the regex is more forgiving than the error message;
     2156  # domain names technically are case-insensitive, and we use printf-like % codes for a couple
     2157  # of types.  Other things may also be added to validate default records of several flavours.
     2158  return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)")
     2159        if $defrec eq 'n' && $$host !~ /^[0-9a-z_%.]+$/i;
     2160
     2161  # Collect these even if we're only doing a simple A record so we can call *any* validation sub
     2162  my $dist = shift;
     2163  my $port = shift;
     2164  my $weight = shift;
     2165
     2166  my $fields;
     2167  my @vallist;
     2168
     2169  # Call the validation sub for the type requested.
     2170  ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id,
     2171        host => $host, rectype => $rectype, val => $val, addr => $addr,
     2172        dist => \$dist, port => \$port, weight => \$weight,
     2173        fields => \$fields, vallist => \@vallist) );
     2174
     2175  return ($retcode,$retmsg) if $retcode eq 'FAIL';
     2176
     2177  # Set up database fields and bind parameters
     2178  $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec);
     2179  push @vallist, ($$host,$$rectype,$$val,$ttl,$id);
     2180  my $vallen = '?'.(',?'x$#vallist);
    14542181
    14552182  # Allow transactions, and raise an exception on errors so we can catch it later.
     
    14592186
    14602187  eval {
    1461     $dbh->do("INSERT INTO ".($defrec eq 'y' ? 'default_' : '')."records ($fields) VALUES ($vallen)",
     2188    $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)",
    14622189        undef, @vallist);
    14632190    $dbh->commit;
     
    14692196  }
    14702197
    1471   return ('OK','OK');
     2198  return ($retcode, $retmsg);
    14722199
    14732200} # end addRec()
     
    15652292  my $dbh = shift;
    15662293  my $defrec = shift;
     2294  my $revrec = shift;
    15672295  my $id = shift;
    15682296
    1569   my $sth = $dbh->prepare("DELETE FROM ".($defrec eq 'y' ? 'default_' : '')."records WHERE record_id=?");
     2297  my $sth = $dbh->prepare("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?");
    15702298  $sth->execute($id);
    15712299
     
    15772305
    15782306  # Reference hashes.
    1579   my %par_tbl = (
     2307my %par_tbl = (
    15802308                group   => 'groups',
    15812309                user    => 'users',
    15822310                defrec  => 'default_records',
     2311                defrevrec       => 'default_rev_records',
    15832312                domain  => 'domains',
     2313                revzone => 'revzones',
    15842314                record  => 'records'
    15852315        );
    1586   my %id_col = (
     2316my %id_col = (
    15872317                group   => 'group_id',
    15882318                user    => 'user_id',
    15892319                defrec  => 'record_id',
     2320                defrevrec       => 'record_id',
    15902321                domain  => 'domain_id',
     2322                revzone => 'rdns_id',
    15912323                record  => 'record_id'
    15922324        );
    1593   my %par_col = (
     2325my %par_col = (
    15942326                group   => 'parent_group_id',
    15952327                user    => 'group_id',
    15962328                defrec  => 'group_id',
     2329                defrevrec       => 'group_id',
    15972330                domain  => 'group_id',
     2331                revzone => 'group_id',
    15982332                record  => 'domain_id'
    15992333        );
    1600   my %par_type = (
     2334my %par_type = (
    16012335                group   => 'group',
    16022336                user    => 'group',
    16032337                defrec  => 'group',
     2338                defrevrec       => 'group',
    16042339                domain  => 'group',
     2340                revzone => 'group',
    16052341                record  => 'domain'
    16062342        );
    16072343
    1608 ## DNSDB::getParents()
    1609 # Find out which entities are parent to the requested id
    1610 # Returns arrayref containing hash pairs of id/type
    1611 sub getParents {
    1612   my $dbh = shift;
    1613   my $id = shift;
    1614   my $type = shift;
    1615   my $depth = shift || 'all';   # valid values:  'all', 'immed', <int> (stop at this group ID)
    1616 
    1617   my @parlist;
    1618 
    1619   while (1) {
    1620     my $result = $dbh->selectrow_hashref("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?",
    1621         undef, ($id) );
    1622     my %tmp = ($result->{$par_col{$type}} => $par_type{$type});
    1623     unshift @parlist, \%tmp;
    1624     last if $result->{$par_col{$type}} == 1;    # group 1 is its own parent
    1625     $id = $result->{$par_col{$type}};
    1626     $type = $par_type{$type};
    1627   }
    1628 
    1629   return \@parlist;
    1630 
    1631 } # end getParents()
     2344
     2345## DNSDB::getTypelist()
     2346# Get a list of record types for various UI dropdowns
     2347# Takes database handle, forward/reverse/lookup flag, and optional "tag as selected" indicator (defaults to A)
     2348# Returns an arrayref to list of hashrefs perfect for HTML::Template
     2349sub getTypelist {
     2350  my $dbh = shift;
     2351  my $recgroup = shift;
     2352  my $type = shift || $reverse_typemap{A};
     2353
     2354  # also accepting $webvar{revrec}!
     2355  $recgroup = 'f' if $recgroup eq 'n';
     2356  $recgroup = 'r' if $recgroup eq 'y';
     2357
     2358  my $sql = "SELECT val,name FROM rectypes WHERE ";
     2359  if ($recgroup eq 'r') {
     2360    # reverse zone types
     2361    $sql .= "stdflag=2 OR stdflag=3";
     2362  } elsif ($recgroup eq 'l') {
     2363    # DNS lookup types.  Note we avoid our custom types >= 65280, since those are entirely internal.
     2364    $sql .= "(stdflag=1 OR stdflag=2 OR stdflag=3) AND val < 65280";
     2365  } else {
     2366    # default;  forward zone types.  technically $type eq 'f' but not worth the error message.
     2367    $sql .= "stdflag=1 OR stdflag=2";
     2368  }
     2369  $sql .= " ORDER BY listorder";
     2370
     2371  my $sth = $dbh->prepare($sql);
     2372  $sth->execute;
     2373  my @typelist;
     2374  while (my ($rval,$rname) = $sth->fetchrow_array()) {
     2375    my %row = ( recval => $rval, recname => $rname );
     2376    $row{tselect} = 1 if $rval == $type;
     2377    push @typelist, \%row;
     2378  }
     2379
     2380  # Add SOA on lookups since it's not listed in other dropdowns.
     2381  if ($recgroup eq 'l') {
     2382    my %row = ( recval => $reverse_typemap{SOA}, recname => 'SOA' );
     2383    $row{tselect} = 1 if $reverse_typemap{SOA} == $type;
     2384    push @typelist, \%row;
     2385  }
     2386
     2387  return \@typelist;
     2388} # end getTypelist()
     2389
     2390
     2391## DNSDB::parentID()
     2392# Get ID of entity that is nearest parent to requested id
     2393# Takes a database handle and a hash of entity ID, entity type, optional parent type flag
     2394# (domain/reverse zone or group), and optional default/live and forward/reverse flags
     2395# Returns the ID or undef on failure
     2396sub parentID {
     2397  my $dbh = shift;
     2398
     2399  my %args = @_;
     2400
     2401  # clean up the parent-type.  Set it to group if not set;  coerce revzone to domain for simpler logic
     2402  $args{partype} = 'group' if !$args{partype};
     2403  $args{partype} = 'domain' if $args{partype} eq 'revzone';
     2404
     2405  # clean up defrec and revrec.  default to live record, forward zone
     2406  $args{defrec} = 'n' if !$args{defrec};
     2407  $args{revrec} = 'n' if !$args{revrec};
     2408
     2409  if ($par_type{$args{partype}} eq 'domain') {
     2410    # only live records can have a domain/zone parent
     2411    return unless ($args{type} eq 'record' && $args{defrec} eq 'n');
     2412    my $result = $dbh->selectrow_hashref("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id').
     2413        " FROM records WHERE record_id = ?",
     2414        undef, ($args{id}) ) or return;
     2415    return $result;
     2416  } else {
     2417    # snag some arguments that will either fall through or be overwritten to save some code duplication
     2418    my $tmpid = $args{id};
     2419    my $type = $args{type};
     2420    if ($type eq 'record' && $args{defrec} eq 'n') {
     2421      # Live records go through the records table first.
     2422      ($tmpid) = $dbh->selectrow_array("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id').
     2423        " FROM records WHERE record_id = ?",
     2424        undef, ($args{id}) ) or return;
     2425      $type = ($args{revrec} eq 'n' ? 'domain' : 'revzone');
     2426    }
     2427    my ($result) = $dbh->selectrow_array("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?",
     2428        undef, ($tmpid) );
     2429    return $result;
     2430  }
     2431# should be impossible to get here with even remotely sane arguments
     2432  return;
     2433} # end parentID()
    16322434
    16332435
     
    16432445
    16442446  # Return false on invalid types
    1645   return 0 if !grep /^$type1$/, ('record','defrec','user','domain','group');
    1646   return 0 if !grep /^$type2$/, ('record','defrec','user','domain','group');
     2447  return 0 if !grep /^$type1$/, ('record','defrec','defrevrec','user','domain','revzone','group');
     2448  return 0 if !grep /^$type2$/, ('record','defrec','defrevrec','user','domain','revzone','group');
    16472449
    16482450  # Return false on impossible relations
    16492451  return 0 if $type1 eq 'record';       # nothing may be a child of a record
    16502452  return 0 if $type1 eq 'defrec';       # nothing may be a child of a record
     2453  return 0 if $type1 eq 'defrevrec';    # nothing may be a child of a record
    16512454  return 0 if $type1 eq 'user';         # nothing may be child of a user
    16522455  return 0 if $type1 eq 'domain' && $type2 ne 'record'; # domain may not be a parent of anything other than a record
     2456  return 0 if $type1 eq 'revzone' && $type2 ne 'record';# reverse zone may not be a parent of anything other than a record
    16532457
    16542458  # ennnhhhh....  if we're passed an id of 0, it will never be found.  usual
    16552459  # case would be the UI creating a new <thing>, and so we don't have an ID for
    16562460  # <thing> to look up yet.  in that case the UI should check the parent as well.
    1657   # argument for returning 1 is
    16582461  return 0 if $id1 == 0;        # nothing can have a parent id of 0
    16592462  return 1 if $id2 == 0;        # anything could have a child id of 0 (or "unknown")
     
    16652468  return 1 if $type1 eq 'group' && $type2 eq 'group' && $id1 == $id2;
    16662469
    1667 # almost the same loop as getParents() above
    16682470  my $id = $id2;
    16692471  my $type = $type2;
    16702472  my $foundparent = 0;
    16712473
     2474  # Records are the only entity with two possible parents.  We need to split the parent checks on
     2475  # domain/rdns.
     2476  if ($type eq 'record') {
     2477    my ($dom,$rdns) = $dbh->selectrow_array("SELECT domain_id,rdns_id FROM records WHERE record_id=?",
     2478        undef, ($id));
     2479    # check immediate parent against request
     2480    return 1 if $type1 eq 'domain' && $id1 == $dom;
     2481    return 1 if $type1 eq 'revzone' && $id1 == $rdns;
     2482    # if request is group, check *both* parents.  Only check if the parent is nonzero though.
     2483    return 1 if $dom && isParent($dbh, $id1, $type1, $dom, 'domain');
     2484    return 1 if $rdns && isParent($dbh, $id1, $type1, $rdns, 'revzone');
     2485    # exit here since we've executed the loop below by proxy in the above recursive calls.
     2486    return 0;
     2487  }
     2488
     2489# almost the same loop as getParents() above
    16722490  my $limiter = 0;
    16732491  while (1) {
     
    16772495    if (!$result) {
    16782496      $limiter++;
    1679 ##fixme:  how often will this happen on a live site?
     2497##fixme:  how often will this happen on a live site?  fail at max limiter <n>?
    16802498      warn "no results looking for $sql with id $id (depth $limiter)\n";
    16812499      last;
     
    16862504    } else {
    16872505##fixme: do we care about trying to return a "no such record/domain/user/group" error?
     2506# should be impossible to create an inconsistent DB just with API calls.
    16882507      warn $dbh->errstr." $sql, $id" if $dbh->errstr;
    16892508    }
Note: See TracChangeset for help on using the changeset viewer.