| [2] | 1 | # dns/trunk/DNSDB.pm | 
|---|
|  | 2 | # Abstraction functions for DNS administration | 
|---|
| [262] | 3 | ## | 
|---|
|  | 4 | # $Id: DNSDB.pm 323 2012-04-29 22:20:17Z kdeugau $ | 
|---|
| [320] | 5 | # Copyright 2008-2012 Kris Deugau <kdeugau@deepnet.cx> | 
|---|
| [262] | 6 | # | 
|---|
|  | 7 | #    This program is free software: you can redistribute it and/or modify | 
|---|
|  | 8 | #    it under the terms of the GNU General Public License as published by | 
|---|
|  | 9 | #    the Free Software Foundation, either version 3 of the License, or | 
|---|
|  | 10 | #    (at your option) any later version. | 
|---|
|  | 11 | # | 
|---|
|  | 12 | #    This program is distributed in the hope that it will be useful, | 
|---|
|  | 13 | #    but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|---|
|  | 14 | #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|---|
|  | 15 | #    GNU General Public License for more details. | 
|---|
|  | 16 | # | 
|---|
|  | 17 | #    You should have received a copy of the GNU General Public License | 
|---|
|  | 18 | #    along with this program.  If not, see <http://www.gnu.org/licenses/>. | 
|---|
|  | 19 | ## | 
|---|
| [2] | 20 |  | 
|---|
|  | 21 | package DNSDB; | 
|---|
|  | 22 |  | 
|---|
|  | 23 | use strict; | 
|---|
|  | 24 | use warnings; | 
|---|
|  | 25 | use Exporter; | 
|---|
|  | 26 | use DBI; | 
|---|
| [33] | 27 | use Net::DNS; | 
|---|
| [65] | 28 | use Crypt::PasswdMD5; | 
|---|
| [198] | 29 | use Net::SMTP; | 
|---|
| [226] | 30 | use NetAddr::IP qw(:lower); | 
|---|
| [198] | 31 | use POSIX; | 
|---|
| [2] | 32 | use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); | 
|---|
|  | 33 |  | 
|---|
| [321] | 34 | $VERSION        = 1.1;  ##VERSION## | 
|---|
| [2] | 35 | @ISA            = qw(Exporter); | 
|---|
|  | 36 | @EXPORT_OK      = qw( | 
|---|
| [279] | 37 | &initGlobals &login &initActionLog | 
|---|
| [67] | 38 | &initPermissions &getPermissions &changePermissions &comparePermissions | 
|---|
| [112] | 39 | &changeGroup | 
|---|
| [128] | 40 | &loadConfig &connectDB &finish | 
|---|
| [276] | 41 | &addDomain &delZone &domainName &revName &domainID &revID &addRDNS | 
|---|
| [237] | 42 | &getZoneCount &getZoneList | 
|---|
| [22] | 43 | &addGroup &delGroup &getChildren &groupName | 
|---|
| [314] | 44 | &getGroupCount &getGroupList | 
|---|
| [83] | 45 | &addUser &updateUser &delUser &userFullName &userStatus &getUserData | 
|---|
| [277] | 46 | &getSOA &updateSOA &getRecLine &getDomRecs &getRecCount | 
|---|
| [22] | 47 | &addRec &updateRec &delRec | 
|---|
| [323] | 48 | &getLogCount &getLogEntries | 
|---|
| [225] | 49 | &getTypelist | 
|---|
| [254] | 50 | &parentID | 
|---|
| [117] | 51 | &isParent | 
|---|
| [275] | 52 | &zoneStatus &importAXFR | 
|---|
| [103] | 53 | &export | 
|---|
| [197] | 54 | &mailNotify | 
|---|
| [128] | 55 | %typemap %reverse_typemap %config | 
|---|
| [66] | 56 | %permissions @permtypes $permlist | 
|---|
| [2] | 57 | ); | 
|---|
|  | 58 |  | 
|---|
|  | 59 | @EXPORT         = (); # Export nothing by default. | 
|---|
|  | 60 | %EXPORT_TAGS    = ( ALL => [qw( | 
|---|
| [279] | 61 | &initGlobals &login &initActionLog | 
|---|
| [67] | 62 | &initPermissions &getPermissions &changePermissions &comparePermissions | 
|---|
| [112] | 63 | &changeGroup | 
|---|
| [128] | 64 | &loadConfig &connectDB &finish | 
|---|
| [276] | 65 | &addDomain &delZone &domainName &revName &domainID &revID &addRDNS | 
|---|
| [237] | 66 | &getZoneCount &getZoneList | 
|---|
| [22] | 67 | &addGroup &delGroup &getChildren &groupName | 
|---|
| [314] | 68 | &getGroupCount &getGroupList | 
|---|
| [83] | 69 | &addUser &updateUser &delUser &userFullName &userStatus &getUserData | 
|---|
| [277] | 70 | &getSOA &updateSOA &getRecLine &getDomRecs &getRecCount | 
|---|
| [22] | 71 | &addRec &updateRec &delRec | 
|---|
| [323] | 72 | &getLogCount &getLogEntries | 
|---|
| [225] | 73 | &getTypelist | 
|---|
| [254] | 74 | &parentID | 
|---|
| [117] | 75 | &isParent | 
|---|
| [275] | 76 | &zoneStatus &importAXFR | 
|---|
| [103] | 77 | &export | 
|---|
| [197] | 78 | &mailNotify | 
|---|
| [128] | 79 | %typemap %reverse_typemap %config | 
|---|
| [66] | 80 | %permissions @permtypes $permlist | 
|---|
| [2] | 81 | )] | 
|---|
|  | 82 | ); | 
|---|
|  | 83 |  | 
|---|
|  | 84 | our $group = 1; | 
|---|
|  | 85 | our $errstr = ''; | 
|---|
| [283] | 86 | our $resultstr = ''; | 
|---|
| [2] | 87 |  | 
|---|
|  | 88 | # Halfway sane defaults for SOA, TTL, etc. | 
|---|
| [101] | 89 | # serial defaults to 0 for convenience. | 
|---|
|  | 90 | # value will be either YYYYMMDDNN for BIND/etc, or auto-internal for tinydns | 
|---|
| [2] | 91 | our %def = qw ( | 
|---|
|  | 92 | contact hostmaster.DOMAIN | 
|---|
|  | 93 | prins   ns1.myserver.com | 
|---|
| [101] | 94 | serial  0 | 
|---|
| [2] | 95 | soattl  86400 | 
|---|
|  | 96 | refresh 10800 | 
|---|
|  | 97 | retry   3600 | 
|---|
|  | 98 | expire  604800 | 
|---|
|  | 99 | minttl  10800 | 
|---|
|  | 100 | ttl     10800 | 
|---|
|  | 101 | ); | 
|---|
|  | 102 |  | 
|---|
| [66] | 103 | # Arguably defined wholly in the db, but little reason to change without supporting code changes | 
|---|
|  | 104 | our @permtypes = qw ( | 
|---|
|  | 105 | group_edit      group_create    group_delete | 
|---|
|  | 106 | user_edit       user_create     user_delete | 
|---|
|  | 107 | domain_edit     domain_create   domain_delete | 
|---|
|  | 108 | record_edit     record_create   record_delete | 
|---|
|  | 109 | self_edit       admin | 
|---|
|  | 110 | ); | 
|---|
|  | 111 | our $permlist = join(',',@permtypes); | 
|---|
|  | 112 |  | 
|---|
| [2] | 113 | # DNS record type map and reverse map. | 
|---|
|  | 114 | # loaded from the database, from http://www.iana.org/assignments/dns-parameters | 
|---|
|  | 115 | our %typemap; | 
|---|
|  | 116 | our %reverse_typemap; | 
|---|
|  | 117 |  | 
|---|
| [65] | 118 | our %permissions; | 
|---|
| [55] | 119 |  | 
|---|
| [128] | 120 | # Prepopulate a basic config.  Note some of these *will* cause errors if left unset. | 
|---|
| [195] | 121 | # note:  add appropriate stanzas in loadConfig to parse these | 
|---|
| [128] | 122 | our %config = ( | 
|---|
|  | 123 | # Database connection info | 
|---|
|  | 124 | dbname  => 'dnsdb', | 
|---|
|  | 125 | dbuser  => 'dnsdb', | 
|---|
|  | 126 | dbpass  => 'secret', | 
|---|
|  | 127 | dbhost  => '', | 
|---|
|  | 128 |  | 
|---|
|  | 129 | # Email notice settings | 
|---|
|  | 130 | mailhost        => 'smtp.example.com', | 
|---|
| [195] | 131 | mailnotify      => 'dnsdb@example.com', # to | 
|---|
|  | 132 | mailsender      => 'dnsdb@example.com', # from | 
|---|
| [128] | 133 | mailname        => 'DNS Administration', | 
|---|
| [195] | 134 | orgname         => 'Example Corp', | 
|---|
|  | 135 | domain          => 'example.com', | 
|---|
| [128] | 136 |  | 
|---|
|  | 137 | # Template directory | 
|---|
|  | 138 | templatedir     => 'templates/', | 
|---|
|  | 139 | # fmeh.  this is a real web path, not a logical internal one.  hm.. | 
|---|
| [163] | 140 | #               cssdir  => 'templates/', | 
|---|
| [216] | 141 | sessiondir      => 'session/', | 
|---|
| [163] | 142 |  | 
|---|
|  | 143 | # Session params | 
|---|
| [195] | 144 | timeout         => '3600',      # 1 hour default | 
|---|
|  | 145 |  | 
|---|
|  | 146 | # Other miscellanea | 
|---|
|  | 147 | log_failures    => 1,   # log all evarthing by default | 
|---|
| [201] | 148 | perpage         => 15, | 
|---|
| [128] | 149 | ); | 
|---|
|  | 150 |  | 
|---|
| [228] | 151 | ## (Semi)private variables | 
|---|
| [278] | 152 |  | 
|---|
| [228] | 153 | # Hash of functions for validating record types.  Filled in initGlobals() since | 
|---|
|  | 154 | # it relies on visibility flags from the rectypes table in the DB | 
|---|
|  | 155 | my %validators; | 
|---|
| [128] | 156 |  | 
|---|
| [278] | 157 | # Username, full name, ID - mainly for logging | 
|---|
|  | 158 | my %userdata; | 
|---|
| [228] | 159 |  | 
|---|
| [295] | 160 | # Entity-relationship reference hashes. | 
|---|
|  | 161 | my %par_tbl = ( | 
|---|
|  | 162 | group   => 'groups', | 
|---|
|  | 163 | user    => 'users', | 
|---|
|  | 164 | defrec  => 'default_records', | 
|---|
|  | 165 | defrevrec       => 'default_rev_records', | 
|---|
|  | 166 | domain  => 'domains', | 
|---|
|  | 167 | revzone => 'revzones', | 
|---|
|  | 168 | record  => 'records' | 
|---|
|  | 169 | ); | 
|---|
|  | 170 | my %id_col = ( | 
|---|
|  | 171 | group   => 'group_id', | 
|---|
|  | 172 | user    => 'user_id', | 
|---|
|  | 173 | defrec  => 'record_id', | 
|---|
|  | 174 | defrevrec       => 'record_id', | 
|---|
|  | 175 | domain  => 'domain_id', | 
|---|
|  | 176 | revzone => 'rdns_id', | 
|---|
|  | 177 | record  => 'record_id' | 
|---|
|  | 178 | ); | 
|---|
|  | 179 | my %par_col = ( | 
|---|
|  | 180 | group   => 'parent_group_id', | 
|---|
|  | 181 | user    => 'group_id', | 
|---|
|  | 182 | defrec  => 'group_id', | 
|---|
|  | 183 | defrevrec       => 'group_id', | 
|---|
|  | 184 | domain  => 'group_id', | 
|---|
|  | 185 | revzone => 'group_id', | 
|---|
|  | 186 | record  => 'domain_id' | 
|---|
|  | 187 | ); | 
|---|
|  | 188 | my %par_type = ( | 
|---|
|  | 189 | group   => 'group', | 
|---|
|  | 190 | user    => 'group', | 
|---|
|  | 191 | defrec  => 'group', | 
|---|
|  | 192 | defrevrec       => 'group', | 
|---|
|  | 193 | domain  => 'group', | 
|---|
|  | 194 | revzone => 'group', | 
|---|
|  | 195 | record  => 'domain' | 
|---|
|  | 196 | ); | 
|---|
| [278] | 197 |  | 
|---|
| [2] | 198 | ## | 
|---|
| [225] | 199 | ## utility functions | 
|---|
| [281] | 200 | ## | 
|---|
|  | 201 |  | 
|---|
|  | 202 | ## DNSDB::_rectable() | 
|---|
| [224] | 203 | # Takes default+rdns flags, returns appropriate table name | 
|---|
|  | 204 | sub _rectable { | 
|---|
|  | 205 | my $def = shift; | 
|---|
|  | 206 | my $rev = shift; | 
|---|
|  | 207 |  | 
|---|
|  | 208 | return 'records' if $def ne 'y'; | 
|---|
|  | 209 | return 'default_records' if $rev ne 'y'; | 
|---|
|  | 210 | return 'default_rev_records'; | 
|---|
|  | 211 | } # end _rectable() | 
|---|
|  | 212 |  | 
|---|
| [281] | 213 | ## DNSDB::_recparent() | 
|---|
| [224] | 214 | # Takes default+rdns flags, returns appropriate parent-id column name | 
|---|
|  | 215 | sub _recparent { | 
|---|
|  | 216 | my $def = shift; | 
|---|
|  | 217 | my $rev = shift; | 
|---|
|  | 218 |  | 
|---|
|  | 219 | return 'group_id' if $def eq 'y'; | 
|---|
|  | 220 | return 'rdns_id' if $rev eq 'y'; | 
|---|
|  | 221 | return 'domain_id'; | 
|---|
|  | 222 | } # end _recparent() | 
|---|
|  | 223 |  | 
|---|
| [281] | 224 | ## DNSDB::_ipparent() | 
|---|
| [226] | 225 | # Check an IP to be added in a reverse zone to see if it's really in the requested parent. | 
|---|
|  | 226 | # Takes a database handle, default and reverse flags, IP (fragment) to check, parent zone ID, | 
|---|
|  | 227 | # and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for | 
|---|
|  | 228 | # database insertion) | 
|---|
|  | 229 | sub _ipparent { | 
|---|
|  | 230 | my $dbh = shift; | 
|---|
|  | 231 | my $defrec = shift; | 
|---|
|  | 232 | my $revrec = shift; | 
|---|
|  | 233 | my $val = shift; | 
|---|
|  | 234 | my $id = shift; | 
|---|
|  | 235 | my $addr = shift; | 
|---|
| [224] | 236 |  | 
|---|
| [232] | 237 | return if $revrec ne 'y';     # this sub not useful in forward zones | 
|---|
|  | 238 |  | 
|---|
|  | 239 | $$addr = NetAddr::IP->new($$val);      #necessary? | 
|---|
|  | 240 |  | 
|---|
| [226] | 241 | # subsub to split, reverse, and overlay an IP fragment on a netblock | 
|---|
|  | 242 | sub __rev_overlay { | 
|---|
|  | 243 | my $splitme = shift;        # ':' or '.', m'lud? | 
|---|
|  | 244 | my $parnet = shift; | 
|---|
|  | 245 | my $val = shift; | 
|---|
|  | 246 | my $addr = shift; | 
|---|
|  | 247 |  | 
|---|
|  | 248 | my $joinme = $splitme; | 
|---|
|  | 249 | $splitme = '\.' if $splitme eq '.'; | 
|---|
| [232] | 250 | my @working = reverse(split($splitme, $parnet->addr)); | 
|---|
|  | 251 | my @parts = reverse(split($splitme, $$val)); | 
|---|
| [226] | 252 | for (my $i = 0; $i <= $#parts; $i++) { | 
|---|
|  | 253 | $working[$i] = $parts[$i]; | 
|---|
|  | 254 | } | 
|---|
| [232] | 255 | my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0; | 
|---|
|  | 256 | return 0 unless $checkme->within($parnet); | 
|---|
| [226] | 257 | $$addr = $checkme;  # force "correct" IP to be recorded. | 
|---|
|  | 258 | return 1; | 
|---|
|  | 259 | } | 
|---|
|  | 260 |  | 
|---|
|  | 261 | my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id)); | 
|---|
|  | 262 | my $parnet = NetAddr::IP->new($parstr); | 
|---|
|  | 263 |  | 
|---|
|  | 264 | # Fail early on v6-in-v4 or v4-in-v6.  We're not accepting these ATM. | 
|---|
| [232] | 265 | return 0 if $parnet->addr =~ /\./ && $$val =~ /:/; | 
|---|
|  | 266 | return 0 if $parnet->addr =~ /:/ && $$val =~ /\./; | 
|---|
| [226] | 267 |  | 
|---|
| [234] | 268 | if ($$addr && $$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/) { | 
|---|
| [232] | 269 | # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address. | 
|---|
|  | 270 | # the rest we have to restructure before fiddling.  *sigh* | 
|---|
|  | 271 | return 1 if $$addr->within($parnet); | 
|---|
|  | 272 | } else { | 
|---|
|  | 273 | # We don't have a complete IP in $$val (yet) | 
|---|
|  | 274 | if ($parnet->addr =~ /:/) { | 
|---|
|  | 275 | $$val =~ s/^:+//;  # gotta strip'em all... | 
|---|
| [226] | 276 | return __rev_overlay(':', $parnet, $val, $addr); | 
|---|
|  | 277 | } | 
|---|
| [232] | 278 | if ($parnet->addr =~ /\./) { | 
|---|
|  | 279 | $$val =~ s/^\.+//; | 
|---|
|  | 280 | return __rev_overlay('.', $parnet, $val, $addr); | 
|---|
|  | 281 | } | 
|---|
| [226] | 282 | # should be impossible to get here... | 
|---|
|  | 283 | } | 
|---|
|  | 284 | # ... and here. | 
|---|
|  | 285 | # can't do nuttin' in forward zones | 
|---|
|  | 286 | } # end _ipparent() | 
|---|
|  | 287 |  | 
|---|
| [281] | 288 | ## DNSDB::_hostparent() | 
|---|
| [232] | 289 | # A little different than _ipparent above;  this tries to *find* the parent zone of a hostname | 
|---|
| [281] | 290 | # Takes a database handle and hostname. | 
|---|
|  | 291 | # Returns the domain ID of the parent domain if one was found. | 
|---|
| [232] | 292 | sub _hostparent { | 
|---|
|  | 293 | my $dbh = shift; | 
|---|
|  | 294 | my $hname = shift; | 
|---|
|  | 295 |  | 
|---|
|  | 296 | my @hostbits = split /\./, $hname; | 
|---|
|  | 297 | my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE domain = ? GROUP BY domain_id"); | 
|---|
|  | 298 | foreach (@hostbits) { | 
|---|
|  | 299 | $sth->execute($hname); | 
|---|
|  | 300 | my ($found, $parid) = $sth->fetchrow_array; | 
|---|
|  | 301 | if ($found) { | 
|---|
|  | 302 | return $parid; | 
|---|
|  | 303 | } | 
|---|
|  | 304 | $hname =~ s/^$_\.//; | 
|---|
|  | 305 | } | 
|---|
|  | 306 | } # end _hostparent() | 
|---|
| [228] | 307 |  | 
|---|
| [281] | 308 | ## DNSDB::_log() | 
|---|
|  | 309 | # Log an action | 
|---|
|  | 310 | # Takes a database handle and log entry hash containing at least: | 
|---|
| [282] | 311 | #  group_id, log entry | 
|---|
| [281] | 312 | # and optionally one or more of: | 
|---|
|  | 313 | #  domain_id, rdns_id | 
|---|
| [282] | 314 | # The %userdata hash provides the user ID, username, and fullname | 
|---|
| [281] | 315 | sub _log { | 
|---|
|  | 316 | my $dbh = shift; | 
|---|
|  | 317 |  | 
|---|
|  | 318 | my %args = @_; | 
|---|
|  | 319 |  | 
|---|
|  | 320 | $args{rdns_id} = 0 if !$args{rdns_id}; | 
|---|
|  | 321 | $args{domain_id} = 0 if !$args{domain_id}; | 
|---|
|  | 322 |  | 
|---|
|  | 323 | ##fixme:  farm out the actual logging to different subs for file, syslog, internal, etc based on config | 
|---|
|  | 324 | #  if ($config{log_channel} eq 'sql') { | 
|---|
|  | 325 | $dbh->do("INSERT INTO log (domain_id,rdns_id,group_id,entry,user_id,email,name) VALUES (?,?,?,?,?,?,?)", | 
|---|
|  | 326 | undef, | 
|---|
|  | 327 | ($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{entry}, | 
|---|
|  | 328 | $userdata{userid}, $userdata{username}, $userdata{fullname}) ); | 
|---|
|  | 329 | #  } elsif ($config{log_channel} eq 'file') { | 
|---|
|  | 330 | #  } elsif ($config{log_channel} eq 'syslog') { | 
|---|
|  | 331 | #  } | 
|---|
|  | 332 | } # end _log | 
|---|
|  | 333 |  | 
|---|
|  | 334 |  | 
|---|
| [224] | 335 | ## | 
|---|
| [228] | 336 | ## Record validation subs. | 
|---|
|  | 337 | ## | 
|---|
|  | 338 |  | 
|---|
| [281] | 339 | ## All of these subs take substantially the same arguments: | 
|---|
|  | 340 | # a database handle | 
|---|
|  | 341 | # a hash containing at least the following keys: | 
|---|
|  | 342 | #  - defrec (default/live flag) | 
|---|
|  | 343 | #  - revrec (forward/reverse flag) | 
|---|
|  | 344 | #  - id (parent entity ID) | 
|---|
|  | 345 | #  - host (hostname) | 
|---|
|  | 346 | #  - rectype | 
|---|
|  | 347 | #  - val (IP, hostname [CNAME/MX/SRV] or text) | 
|---|
|  | 348 | #  - addr (NetAddr::IP object from val.  May be undef.) | 
|---|
|  | 349 | # MX and SRV record validation also expect distance, and SRV records expect weight and port as well. | 
|---|
|  | 350 | # host, rectype, and addr should be references as these may be modified in validation | 
|---|
|  | 351 |  | 
|---|
| [228] | 352 | # A record | 
|---|
|  | 353 | sub _validate_1 { | 
|---|
| [229] | 354 | my $dbh = shift; | 
|---|
|  | 355 |  | 
|---|
| [230] | 356 | my %args = @_; | 
|---|
| [229] | 357 |  | 
|---|
| [230] | 358 | return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y'; | 
|---|
| [229] | 359 |  | 
|---|
|  | 360 | # Coerce all hostnames to end in ".DOMAIN" for group/default records, | 
|---|
|  | 361 | # or the intended parent domain for live records. | 
|---|
| [230] | 362 | my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); | 
|---|
|  | 363 | ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; | 
|---|
| [229] | 364 |  | 
|---|
|  | 365 | # Check IP is well-formed, and that it's a v4 address | 
|---|
| [234] | 366 | # Fail on "compact" IPv4 variants, because they are not consistent and predictable. | 
|---|
| [232] | 367 | return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") | 
|---|
| [234] | 368 | unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/; | 
|---|
|  | 369 | return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") | 
|---|
| [230] | 370 | unless $args{addr} && !$args{addr}->{isv6}; | 
|---|
| [229] | 371 | # coerce IP/value to normalized form for storage | 
|---|
| [230] | 372 | ${$args{val}} = $args{addr}->addr; | 
|---|
| [229] | 373 |  | 
|---|
| [228] | 374 | return ('OK','OK'); | 
|---|
|  | 375 | } # done A record | 
|---|
|  | 376 |  | 
|---|
|  | 377 | # NS record | 
|---|
|  | 378 | sub _validate_2 { | 
|---|
| [230] | 379 | my $dbh = shift; | 
|---|
|  | 380 |  | 
|---|
|  | 381 | my %args = @_; | 
|---|
|  | 382 |  | 
|---|
|  | 383 | # Coerce the hostname to "DOMAIN" for forward default records, "ZONE" for reverse default records, | 
|---|
|  | 384 | # or the intended parent zone for live records. | 
|---|
|  | 385 | ##fixme:  allow for delegating <subdomain>.DOMAIN? | 
|---|
|  | 386 | if ($args{revrec} eq 'y') { | 
|---|
|  | 387 | my $pname = ($args{defrec} eq 'y' ? 'ZONE' : revName($dbh,$args{id})); | 
|---|
|  | 388 | ${$args{host}} = $pname if ${$args{host}} ne $pname; | 
|---|
|  | 389 | } else { | 
|---|
|  | 390 | my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); | 
|---|
|  | 391 | ${$args{host}} = $pname if ${$args{host}} ne $pname; | 
|---|
|  | 392 | } | 
|---|
|  | 393 |  | 
|---|
|  | 394 | # Let this lie for now.  Needs more magic. | 
|---|
|  | 395 | #  # Check IP is well-formed, and that it's a v4 address | 
|---|
|  | 396 | #  return ('FAIL',"A record must be a valid IPv4 address") | 
|---|
|  | 397 | #       unless $addr && !$addr->{isv6}; | 
|---|
|  | 398 | #  # coerce IP/value to normalized form for storage | 
|---|
|  | 399 | #  $$val = $addr->addr; | 
|---|
|  | 400 |  | 
|---|
| [228] | 401 | return ('OK','OK'); | 
|---|
|  | 402 | } # done NS record | 
|---|
|  | 403 |  | 
|---|
|  | 404 | # CNAME record | 
|---|
|  | 405 | sub _validate_5 { | 
|---|
| [230] | 406 | my $dbh = shift; | 
|---|
|  | 407 |  | 
|---|
|  | 408 | my %args = @_; | 
|---|
|  | 409 |  | 
|---|
|  | 410 | # Not really true, but these are only useful for delegating smaller-than-/24 IP blocks. | 
|---|
|  | 411 | # This is fundamentally a messy operation and should really just be taken care of by the | 
|---|
|  | 412 | # export process, not manual maintenance of the necessary records. | 
|---|
|  | 413 | return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y'; | 
|---|
|  | 414 |  | 
|---|
|  | 415 | # Coerce all hostnames to end in ".DOMAIN" for group/default records, | 
|---|
|  | 416 | # or the intended parent domain for live records. | 
|---|
|  | 417 | my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); | 
|---|
|  | 418 | ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; | 
|---|
|  | 419 |  | 
|---|
| [228] | 420 | return ('OK','OK'); | 
|---|
|  | 421 | } # done CNAME record | 
|---|
|  | 422 |  | 
|---|
|  | 423 | # SOA record | 
|---|
|  | 424 | sub _validate_6 { | 
|---|
| [230] | 425 | # Smart monkeys won't stick their fingers in here;  we have | 
|---|
|  | 426 | # separate dedicated routines to deal with SOA records. | 
|---|
| [228] | 427 | return ('OK','OK'); | 
|---|
|  | 428 | } # done SOA record | 
|---|
|  | 429 |  | 
|---|
|  | 430 | # PTR record | 
|---|
|  | 431 | sub _validate_12 { | 
|---|
| [232] | 432 | my $dbh = shift; | 
|---|
|  | 433 |  | 
|---|
|  | 434 | my %args = @_; | 
|---|
|  | 435 |  | 
|---|
|  | 436 | if ($args{revrec} eq 'y') { | 
|---|
|  | 437 | if ($args{defrec} eq 'n') { | 
|---|
|  | 438 | return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id})) | 
|---|
|  | 439 | unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr}); | 
|---|
|  | 440 | ${$args{val}} = $args{addr}->addr; | 
|---|
|  | 441 | } else { | 
|---|
| [234] | 442 | if (${$args{val}} =~ /\./) { | 
|---|
|  | 443 | # looks like a v4 or fragment | 
|---|
|  | 444 | if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) { | 
|---|
|  | 445 | # woo!  a complete IP!  validate it and normalize, or fail. | 
|---|
|  | 446 | $args{addr} = NetAddr::IP->new(${$args{val}}) | 
|---|
|  | 447 | or return ('FAIL', "IP/value looks like IPv4 but isn't valid"); | 
|---|
|  | 448 | ${$args{val}} = $args{addr}->addr; | 
|---|
|  | 449 | } else { | 
|---|
| [249] | 450 | ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/; | 
|---|
| [234] | 451 | } | 
|---|
|  | 452 | } elsif (${$args{val}} =~ /[a-f:]/) { | 
|---|
|  | 453 | # looks like a v6 or fragment | 
|---|
| [251] | 454 | ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/; | 
|---|
| [234] | 455 | if ($args{addr}) { | 
|---|
|  | 456 | if ($args{addr}->addr =~ /^0/) { | 
|---|
| [251] | 457 | ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/; | 
|---|
| [234] | 458 | } else { | 
|---|
|  | 459 | ${$args{val}} = $args{addr}->addr; | 
|---|
|  | 460 | } | 
|---|
|  | 461 | } | 
|---|
|  | 462 | } else { | 
|---|
|  | 463 | # bare number (probably).  These could be v4 or v6, so we'll | 
|---|
|  | 464 | # expand on these on creation of a reverse zone. | 
|---|
| [251] | 465 | ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/; | 
|---|
| [234] | 466 | } | 
|---|
| [249] | 467 | ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/; | 
|---|
| [232] | 468 | } | 
|---|
|  | 469 |  | 
|---|
|  | 470 | # Multiple PTR records do NOT generally do what most people believe they do, | 
|---|
|  | 471 | # and tend to fail in the most awkward way possible.  Check and warn. | 
|---|
|  | 472 | # We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12" | 
|---|
| [249] | 473 |  | 
|---|
|  | 474 | my @checkvals = (${$args{val}}); | 
|---|
|  | 475 | if (${$args{val}} =~ /,/) { | 
|---|
|  | 476 | # push . and :: variants into checkvals if val has , | 
|---|
|  | 477 | my $tmp; | 
|---|
|  | 478 | ($tmp = ${$args{val}}) =~ s/,/./; | 
|---|
|  | 479 | push @checkvals, $tmp; | 
|---|
|  | 480 | ($tmp = ${$args{val}}) =~ s/,/::/; | 
|---|
|  | 481 | push @checkvals, $tmp; | 
|---|
|  | 482 | } | 
|---|
|  | 483 | my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?"); | 
|---|
|  | 484 | foreach my $checkme (@checkvals) { | 
|---|
| [272] | 485 | if ($args{update}) { | 
|---|
|  | 486 | # Record update.  There should usually be an existing PTR (the record being updated) | 
|---|
|  | 487 | my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}). | 
|---|
|  | 488 | " WHERE val = ?", undef, ($checkme)) }; | 
|---|
|  | 489 | return ('WARN', "PTR record for $checkme already exists;  adding another will probably not do what you want") | 
|---|
| [273] | 490 | if @ptrs && (!grep /^$args{update}$/, @ptrs); | 
|---|
| [272] | 491 | } else { | 
|---|
|  | 492 | # New record.  Always warn if a PTR exists | 
|---|
|  | 493 | my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). | 
|---|
|  | 494 | " WHERE val = ?", undef, ($checkme)); | 
|---|
|  | 495 | return ('WARN', "PTR record for $checkme already exists;  adding another will probably not do what you want") | 
|---|
|  | 496 | if $ptrcount; | 
|---|
|  | 497 | } | 
|---|
| [249] | 498 | } | 
|---|
| [272] | 499 |  | 
|---|
| [232] | 500 | } else { | 
|---|
|  | 501 | # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations | 
|---|
|  | 502 | # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct | 
|---|
|  | 503 | # PTR records on export | 
|---|
|  | 504 | return ('FAIL',"Forward zones cannot contain PTR records"); | 
|---|
|  | 505 | } | 
|---|
|  | 506 |  | 
|---|
| [228] | 507 | return ('OK','OK'); | 
|---|
|  | 508 | } # done PTR record | 
|---|
|  | 509 |  | 
|---|
|  | 510 | # MX record | 
|---|
|  | 511 | sub _validate_15 { | 
|---|
| [230] | 512 | my $dbh = shift; | 
|---|
|  | 513 |  | 
|---|
|  | 514 | my %args = @_; | 
|---|
|  | 515 |  | 
|---|
|  | 516 | # Not absolutely true but WTF use is an MX record for a reverse zone? | 
|---|
|  | 517 | return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y'; | 
|---|
|  | 518 |  | 
|---|
|  | 519 | return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}}); | 
|---|
|  | 520 | ${$args{dist}} =~ s/\s*//g; | 
|---|
|  | 521 | return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; | 
|---|
|  | 522 |  | 
|---|
|  | 523 | ${$args{fields}} = "distance,"; | 
|---|
|  | 524 | push @{$args{vallist}}, ${$args{dist}}; | 
|---|
|  | 525 |  | 
|---|
|  | 526 | # Coerce all hostnames to end in ".DOMAIN" for group/default records, | 
|---|
|  | 527 | # or the intended parent domain for live records. | 
|---|
|  | 528 | my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); | 
|---|
|  | 529 | ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; | 
|---|
|  | 530 |  | 
|---|
| [273] | 531 | # hmm..  this might work.  except possibly for something pointing to "deadbeef.ca".  <g> | 
|---|
|  | 532 | #  if ($type == $reverse_typemap{NS} || $type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) { | 
|---|
|  | 533 | #    if ($val =~ /^\s*[\da-f:.]+\s*$/) { | 
|---|
|  | 534 | #      return ('FAIL',"$val is not a valid IP address") if !$addr; | 
|---|
|  | 535 | #    } | 
|---|
|  | 536 | #  } | 
|---|
|  | 537 |  | 
|---|
| [228] | 538 | return ('OK','OK'); | 
|---|
|  | 539 | } # done MX record | 
|---|
|  | 540 |  | 
|---|
|  | 541 | # TXT record | 
|---|
|  | 542 | sub _validate_16 { | 
|---|
| [231] | 543 | # Could arguably put a WARN return here on very long (>512) records | 
|---|
| [228] | 544 | return ('OK','OK'); | 
|---|
|  | 545 | } # done TXT record | 
|---|
|  | 546 |  | 
|---|
|  | 547 | # RP record | 
|---|
|  | 548 | sub _validate_17 { | 
|---|
| [231] | 549 | # Probably have to validate these some day | 
|---|
| [228] | 550 | return ('OK','OK'); | 
|---|
|  | 551 | } # done RP record | 
|---|
|  | 552 |  | 
|---|
|  | 553 | # AAAA record | 
|---|
|  | 554 | sub _validate_28 { | 
|---|
| [229] | 555 | my $dbh = shift; | 
|---|
|  | 556 |  | 
|---|
| [230] | 557 | my %args = @_; | 
|---|
| [229] | 558 |  | 
|---|
| [230] | 559 | return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y'; | 
|---|
| [229] | 560 |  | 
|---|
|  | 561 | # Coerce all hostnames to end in ".DOMAIN" for group/default records, | 
|---|
|  | 562 | # or the intended parent domain for live records. | 
|---|
| [230] | 563 | my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); | 
|---|
|  | 564 | ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; | 
|---|
| [229] | 565 |  | 
|---|
|  | 566 | # Check IP is well-formed, and that it's a v6 address | 
|---|
| [232] | 567 | return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address") | 
|---|
| [230] | 568 | unless $args{addr} && $args{addr}->{isv6}; | 
|---|
| [229] | 569 | # coerce IP/value to normalized form for storage | 
|---|
| [230] | 570 | ${$args{val}} = $args{addr}->addr; | 
|---|
| [229] | 571 |  | 
|---|
| [228] | 572 | return ('OK','OK'); | 
|---|
|  | 573 | } # done AAAA record | 
|---|
|  | 574 |  | 
|---|
|  | 575 | # SRV record | 
|---|
|  | 576 | sub _validate_33 { | 
|---|
| [231] | 577 | my $dbh = shift; | 
|---|
|  | 578 |  | 
|---|
|  | 579 | my %args = @_; | 
|---|
|  | 580 |  | 
|---|
|  | 581 | # Not absolutely true but WTF use is an SRV record for a reverse zone? | 
|---|
|  | 582 | return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y'; | 
|---|
|  | 583 |  | 
|---|
|  | 584 | return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}}); | 
|---|
|  | 585 | ${$args{dist}} =~ s/\s*//g; | 
|---|
|  | 586 | return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; | 
|---|
|  | 587 |  | 
|---|
|  | 588 | return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]") | 
|---|
|  | 589 | unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/; | 
|---|
|  | 590 | return ('FAIL',"Port and weight are required for SRV records") | 
|---|
|  | 591 | unless defined(${$args{weight}}) && defined(${$args{port}}); | 
|---|
|  | 592 | ${$args{weight}} =~ s/\s*//g; | 
|---|
|  | 593 | ${$args{port}} =~ s/\s*//g; | 
|---|
|  | 594 |  | 
|---|
|  | 595 | return ('FAIL',"Port and weight are required, and must be numeric") | 
|---|
|  | 596 | unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/; | 
|---|
|  | 597 |  | 
|---|
|  | 598 | ${$args{fields}} = "distance,weight,port,"; | 
|---|
|  | 599 | push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}}); | 
|---|
|  | 600 |  | 
|---|
|  | 601 | # Coerce all hostnames to end in ".DOMAIN" for group/default records, | 
|---|
|  | 602 | # or the intended parent domain for live records. | 
|---|
|  | 603 | my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); | 
|---|
|  | 604 | ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; | 
|---|
|  | 605 |  | 
|---|
| [228] | 606 | return ('OK','OK'); | 
|---|
|  | 607 | } # done SRV record | 
|---|
|  | 608 |  | 
|---|
|  | 609 | # Now the custom types | 
|---|
|  | 610 |  | 
|---|
| [232] | 611 | # A+PTR record.  With a very little bit of magic we can also use this sub to validate AAAA+PTR.  Whee! | 
|---|
| [228] | 612 | sub _validate_65280 { | 
|---|
| [232] | 613 | my $dbh = shift; | 
|---|
|  | 614 |  | 
|---|
|  | 615 | my %args = @_; | 
|---|
|  | 616 |  | 
|---|
|  | 617 | my $code = 'OK'; | 
|---|
|  | 618 | my $msg = 'OK'; | 
|---|
|  | 619 |  | 
|---|
|  | 620 | if ($args{defrec} eq 'n') { | 
|---|
|  | 621 | # live record;  revrec determines whether we validate the PTR or A component first. | 
|---|
| [233] | 622 |  | 
|---|
| [232] | 623 | if ($args{revrec} eq 'y') { | 
|---|
|  | 624 | ($code,$msg) = _validate_12($dbh, %args); | 
|---|
|  | 625 | return ($code,$msg) if $code eq 'FAIL'; | 
|---|
|  | 626 |  | 
|---|
|  | 627 | # Check if the reqested domain exists.  If not, coerce the type down to PTR and warn. | 
|---|
|  | 628 | if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) { | 
|---|
| [272] | 629 | my $addmsg = "Record ".($args{update} ? 'updated' : 'added'). | 
|---|
|  | 630 | " as PTR instead of $typemap{${$args{rectype}}};  domain not found for ${$args{host}}"; | 
|---|
| [232] | 631 | $msg .= "\n$addmsg" if $code eq 'WARN'; | 
|---|
|  | 632 | $msg = $addmsg if $code eq 'OK'; | 
|---|
|  | 633 | ${$args{rectype}} = $reverse_typemap{PTR}; | 
|---|
|  | 634 | return ('WARN', $msg); | 
|---|
|  | 635 | } | 
|---|
|  | 636 |  | 
|---|
| [242] | 637 | # Add domain ID to field list and values | 
|---|
|  | 638 | ${$args{fields}} .= "domain_id,"; | 
|---|
|  | 639 | push @{$args{vallist}}, ${$args{domid}}; | 
|---|
|  | 640 |  | 
|---|
| [232] | 641 | } else { | 
|---|
|  | 642 | ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280; | 
|---|
|  | 643 | ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281; | 
|---|
|  | 644 | return ($code,$msg) if $code eq 'FAIL'; | 
|---|
|  | 645 |  | 
|---|
|  | 646 | # Check if the requested reverse zone exists - note, an IP fragment won't | 
|---|
|  | 647 | # work here since we don't *know* which parent to put it in. | 
|---|
| [233] | 648 | # ${$args{val}} has been validated as a valid IP by now, in one of the above calls. | 
|---|
| [232] | 649 | my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?". | 
|---|
|  | 650 | " ORDER BY masklen(revnet) DESC", undef, (${$args{val}})); | 
|---|
|  | 651 | if (!$revid) { | 
|---|
| [272] | 652 | $msg = "Record ".($args{update} ? 'updated' : 'added')." as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA'). | 
|---|
| [232] | 653 | " instead of $typemap{${$args{rectype}}};  reverse zone not found for ${$args{val}}"; | 
|---|
|  | 654 | ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA}); | 
|---|
|  | 655 | return ('WARN', $msg); | 
|---|
|  | 656 | } | 
|---|
|  | 657 |  | 
|---|
|  | 658 | # Check for duplicate PTRs.  Note we don't have to play games with $code and $msg, because | 
|---|
|  | 659 | # by definition there can't be duplicate PTRs if the reverse zone isn't managed here. | 
|---|
| [272] | 660 | if ($args{update}) { | 
|---|
|  | 661 | # Record update.  There should usually be an existing PTR (the record being updated) | 
|---|
|  | 662 | my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}). | 
|---|
|  | 663 | " WHERE val = ?", undef, (${$args{val}})) }; | 
|---|
| [273] | 664 | if (@ptrs && (!grep /^$args{update}$/, @ptrs)) { | 
|---|
| [272] | 665 | $msg = "PTR record for ${$args{val}} already exists;  adding another will probably not do what you want"; | 
|---|
|  | 666 | $code = 'WARN'; | 
|---|
|  | 667 | } | 
|---|
|  | 668 | } else { | 
|---|
|  | 669 | # New record.  Always warn if a PTR exists | 
|---|
|  | 670 | my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). | 
|---|
|  | 671 | " WHERE val = ?", undef, (${$args{val}})); | 
|---|
|  | 672 | $msg = "PTR record for ${$args{val}} already exists;  adding another will probably not do what you want" | 
|---|
|  | 673 | if $ptrcount; | 
|---|
|  | 674 | $code = 'WARN' if $ptrcount; | 
|---|
| [232] | 675 | } | 
|---|
|  | 676 |  | 
|---|
| [272] | 677 | #      my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). | 
|---|
|  | 678 | #       " WHERE val = ?", undef, ${$args{val}}); | 
|---|
|  | 679 | #      if ($ptrcount) { | 
|---|
|  | 680 | #        my $curid = $dbh->selectrow_array("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}). | 
|---|
|  | 681 | #               " WHERE val = ? | 
|---|
|  | 682 | #       $msg = "PTR record for ${$args{val}} already exists;  adding another will probably not do what you want"; | 
|---|
|  | 683 | #       $code = 'WARN'; | 
|---|
|  | 684 | #      } | 
|---|
|  | 685 |  | 
|---|
| [232] | 686 | ${$args{fields}} .= "rdns_id,"; | 
|---|
|  | 687 | push @{$args{vallist}}, $revid; | 
|---|
|  | 688 | } | 
|---|
|  | 689 |  | 
|---|
| [233] | 690 | } else {      # defrec eq 'y' | 
|---|
|  | 691 | if ($args{revrec} eq 'y') { | 
|---|
|  | 692 | ($code,$msg) = _validate_12($dbh, %args); | 
|---|
|  | 693 | return ($code,$msg) if $code eq 'FAIL'; | 
|---|
|  | 694 | if (${$args{rectype}} == 65280) { | 
|---|
|  | 695 | return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment") | 
|---|
|  | 696 | if ${$args{val}} =~ /:/; | 
|---|
| [234] | 697 | ${$args{val}} =~ s/^ZONE,/ZONE./;       # Clean up after uncertain IP-fragment-type from _validate_12 | 
|---|
| [233] | 698 | } elsif (${$args{rectype}} == 65281) { | 
|---|
|  | 699 | return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment") | 
|---|
|  | 700 | if ${$args{val}} =~ /\./; | 
|---|
| [234] | 701 | ${$args{val}} =~ s/^ZONE,/ZONE::/;      # Clean up after uncertain IP-fragment-type from _validate_12 | 
|---|
| [233] | 702 | } | 
|---|
|  | 703 | } else { | 
|---|
|  | 704 | # This is easy.  I also can't see a real use-case for A/AAAA+PTR in *all* forward | 
|---|
|  | 705 | # domains, since you wouldn't be able to substitute both domain and reverse zone | 
|---|
|  | 706 | # sanely, and you'd end up with guaranteed over-replicated PTR records that would | 
|---|
|  | 707 | # confuse the hell out of pretty much anything that uses them. | 
|---|
| [234] | 708 | ##fixme: make this a config flag? | 
|---|
| [233] | 709 | return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains"); | 
|---|
|  | 710 | } | 
|---|
| [232] | 711 | } | 
|---|
|  | 712 |  | 
|---|
|  | 713 | return ($code, $msg); | 
|---|
| [228] | 714 | } # done A+PTR record | 
|---|
|  | 715 |  | 
|---|
|  | 716 | # AAAA+PTR record | 
|---|
| [232] | 717 | # A+PTR above has been magicked to handle AAAA+PTR as well. | 
|---|
| [228] | 718 | sub _validate_65281 { | 
|---|
| [232] | 719 | return _validate_65280(@_); | 
|---|
| [228] | 720 | } # done AAAA+PTR record | 
|---|
|  | 721 |  | 
|---|
|  | 722 | # PTR template record | 
|---|
|  | 723 | sub _validate_65282 { | 
|---|
|  | 724 | return ('OK','OK'); | 
|---|
|  | 725 | } # done PTR template record | 
|---|
|  | 726 |  | 
|---|
|  | 727 | # A+PTR template record | 
|---|
|  | 728 | sub _validate_65283 { | 
|---|
|  | 729 | return ('OK','OK'); | 
|---|
|  | 730 | } # done AAAA+PTR template record | 
|---|
|  | 731 |  | 
|---|
|  | 732 | # AAAA+PTR template record | 
|---|
|  | 733 | sub _validate_65284 { | 
|---|
|  | 734 | return ('OK','OK'); | 
|---|
|  | 735 | } # done AAAA+PTR template record | 
|---|
|  | 736 |  | 
|---|
|  | 737 |  | 
|---|
| [265] | 738 | ## | 
|---|
|  | 739 | ## Record data substitution subs | 
|---|
|  | 740 | ## | 
|---|
| [228] | 741 |  | 
|---|
| [298] | 742 | # Replace ZONE in hostname, or create (most of) the actual proper zone name | 
|---|
| [265] | 743 | sub _ZONE { | 
|---|
|  | 744 | my $zone = shift; | 
|---|
|  | 745 | my $string = shift; | 
|---|
|  | 746 | my $fr = shift || 'f';        # flag for forward/reverse order?  nb: ignored for IP | 
|---|
| [298] | 747 | my $sep = shift || '-';       # Separator character - unlikely we'll ever need more than . or - | 
|---|
| [265] | 748 |  | 
|---|
| [298] | 749 | my $prefix; | 
|---|
| [265] | 750 |  | 
|---|
|  | 751 | $string =~ s/,/./ if !$zone->{isv6}; | 
|---|
|  | 752 | $string =~ s/,/::/ if $zone->{isv6}; | 
|---|
|  | 753 |  | 
|---|
| [298] | 754 | # Subbing ZONE in the host.  We need to properly ID the netblock range | 
|---|
|  | 755 | # The subbed text should have "network IP with trailing zeros stripped" for | 
|---|
|  | 756 | # blocks lined up on octet (for v4) or hex-quad (for v6) boundaries | 
|---|
|  | 757 | # For blocks that do NOT line up on these boundaries, we take the most | 
|---|
|  | 758 | # significant octet or 16-bit chunk of the "broadcast" IP and append it | 
|---|
|  | 759 | # after a double-dash | 
|---|
|  | 760 | # ie: | 
|---|
|  | 761 | # 8.0.0.0/6 -> 8.0.0.0 -> 11.255.255.255;  sub should be 8--11 | 
|---|
|  | 762 | # 10.0.0.0/12 -> 10.0.0.0 -> 10.0.0.0 -> 10.15.255.255;  sub should be 10-0--15 | 
|---|
|  | 763 | # 192.168.4.0/22 -> 192.168.4.0 -> 192.168.7.255;  sub should be 192-168-4--7 | 
|---|
|  | 764 | # 192.168.0.8/29 -> 192.168.0.8 -> 192.168.0.15;  sub should be 192-168-0-8--15 | 
|---|
|  | 765 | # Similar for v6 | 
|---|
| [265] | 766 |  | 
|---|
| [298] | 767 | if (!$zone->{isv6}) { # IPv4 | 
|---|
|  | 768 |  | 
|---|
|  | 769 | $prefix = $zone->network->addr;     # Just In Case someone managed to slip in | 
|---|
|  | 770 | # a funky subnet that had host bits set. | 
|---|
|  | 771 | my $bc = $zone->broadcast->addr; | 
|---|
|  | 772 |  | 
|---|
|  | 773 | if ($zone->masklen > 24) { | 
|---|
|  | 774 | $bc =~ s/^\d+\.\d+\.\d+\.//; | 
|---|
|  | 775 | } elsif ($zone->masklen > 16) { | 
|---|
|  | 776 | $prefix =~ s/\.0$//; | 
|---|
|  | 777 | $bc =~ s/^\d+\.\d+\.//; | 
|---|
|  | 778 | } elsif ($zone->masklen > 8) { | 
|---|
|  | 779 | $bc =~ s/^\d+\.//; | 
|---|
|  | 780 | $prefix =~ s/\.0\.0$//; | 
|---|
|  | 781 | } else { | 
|---|
|  | 782 | $prefix =~ s/\.0\.0\.0$//; | 
|---|
|  | 783 | } | 
|---|
|  | 784 | if ($zone->masklen % 8) { | 
|---|
|  | 785 | $bc =~ s/(\.255)+$//; | 
|---|
|  | 786 | $prefix .= "--$bc";       #"--".zone->masklen;    # use range or mask length? | 
|---|
|  | 787 | } | 
|---|
|  | 788 | if ($fr eq 'f') { | 
|---|
|  | 789 | $prefix =~ s/\.+/$sep/g; | 
|---|
|  | 790 | } else { | 
|---|
|  | 791 | $prefix = join($sep, reverse(split(/\./, $prefix))); | 
|---|
|  | 792 | } | 
|---|
|  | 793 |  | 
|---|
|  | 794 | } else { # IPv6 | 
|---|
|  | 795 |  | 
|---|
|  | 796 | if ($fr eq 'f') { | 
|---|
|  | 797 |  | 
|---|
|  | 798 | $prefix = $zone->network->addr;   # Just In Case someone managed to slip in | 
|---|
|  | 799 | # a funky subnet that had host bits set. | 
|---|
| [265] | 800 | my $bc = $zone->broadcast->addr; | 
|---|
|  | 801 | if (($zone->masklen % 16) != 0) { | 
|---|
| [298] | 802 | # Strip trailing :0 off $prefix, and :ffff off the broadcast IP | 
|---|
|  | 803 | for (my $i=0; $i<(7-int($zone->masklen / 16)); $i++) { | 
|---|
|  | 804 | $prefix =~ s/:0$//; | 
|---|
|  | 805 | $bc =~ s/:ffff$//; | 
|---|
|  | 806 | } | 
|---|
|  | 807 | # Strip the leading 16-bit chunks off the front of the broadcast IP | 
|---|
|  | 808 | $bc =~ s/^([a-f0-9]+:)+//; | 
|---|
|  | 809 | # Append the remaining 16-bit chunk to the prefix after "--" | 
|---|
|  | 810 | $prefix .= "--$bc"; | 
|---|
| [265] | 811 | } else { | 
|---|
| [298] | 812 | # Strip off :0 from the end until we reach the netblock length. | 
|---|
|  | 813 | for (my $i=0; $i<(8-$zone->masklen / 16); $i++) { | 
|---|
| [265] | 814 | $prefix =~ s/:0$//; | 
|---|
| [298] | 815 | } | 
|---|
| [265] | 816 | } | 
|---|
| [298] | 817 | # Actually deal with the separator | 
|---|
|  | 818 | $prefix =~ s/:/$sep/g; | 
|---|
|  | 819 |  | 
|---|
|  | 820 | } else {    # $fr eq 'f' | 
|---|
|  | 821 |  | 
|---|
|  | 822 | $prefix = $zone->network->full;   # Just In Case someone managed to slip in | 
|---|
|  | 823 | # a funky subnet that had host bits set. | 
|---|
|  | 824 | my $bc = $zone->broadcast->full; | 
|---|
|  | 825 | $prefix =~ s/://g;        # clean these out since they're not spaced right for this case | 
|---|
|  | 826 | $bc =~ s/://g; | 
|---|
|  | 827 | # Strip trailing 0 off $prefix, and f off the broadcast IP, to match the mask length | 
|---|
|  | 828 | for (my $i=0; $i<(31-int($zone->masklen / 4)); $i++) { | 
|---|
|  | 829 | $prefix =~ s/0$//; | 
|---|
|  | 830 | $bc =~ s/f$//; | 
|---|
|  | 831 | } | 
|---|
|  | 832 | # Split and reverse the order of the nibbles in the network/broadcast IPs | 
|---|
| [312] | 833 | $prefix =~ s/0$// if $zone->masklen % 4 == 0;     # trim another 0 for nibble-aligned blocks first | 
|---|
| [298] | 834 | my @nbits = reverse split //, $prefix; | 
|---|
|  | 835 | my @bbits = reverse split //, $bc; | 
|---|
|  | 836 | # Handle the sub-nibble case.  Eww.  I feel dirty supporting this... | 
|---|
|  | 837 | $nbits[0] = "$nbits[0]-$bbits[0]" if ($zone->masklen % 4) != 0; | 
|---|
|  | 838 | # Glue it back together | 
|---|
|  | 839 | $prefix = join($sep, @nbits); | 
|---|
|  | 840 |  | 
|---|
|  | 841 | }   # $fr ne 'f' | 
|---|
|  | 842 |  | 
|---|
|  | 843 | } # $zone->{isv6} | 
|---|
|  | 844 |  | 
|---|
|  | 845 | # Do the substitution, finally | 
|---|
|  | 846 | $string =~ s/ZONE/$prefix/; | 
|---|
|  | 847 | $string =~ s/--/-/ if $sep ne '-';    # - as separator needs extra help for sub-octet v4 netblocks | 
|---|
|  | 848 | return $string; | 
|---|
|  | 849 | } # done _ZONE() | 
|---|
|  | 850 |  | 
|---|
|  | 851 | # Not quite a substitution sub, but placed here as it's basically the inverse of above; | 
|---|
|  | 852 | # given the .arpa zone name, return the CIDR netblock the zone is for. | 
|---|
| [304] | 853 | # Supports v4 non-octet/non-classful netblocks as per the method outlined in the Grasshopper Book (2nd Ed p217-218) | 
|---|
| [298] | 854 | # Does NOT support non-quad v6 netblocks via the same scheme;  it shouldn't ever be necessary. | 
|---|
|  | 855 | # Takes a nominal .arpa zone name, returns a success code and NetAddr::IP, or a fail code and message | 
|---|
|  | 856 | sub _zone2cidr { | 
|---|
|  | 857 | my $zone = shift; | 
|---|
|  | 858 |  | 
|---|
|  | 859 | my $cidr; | 
|---|
| [301] | 860 | my $tmpcidr; | 
|---|
|  | 861 | my $warnmsg = ''; | 
|---|
| [298] | 862 |  | 
|---|
|  | 863 | if ($zone =~ /\.in-addr\.arpa\.?$/) { | 
|---|
|  | 864 | # v4 revzone, formal zone name type | 
|---|
|  | 865 | my $tmpzone = $zone; | 
|---|
|  | 866 | $tmpzone =~ s/\.in-addr\.arpa\.?//; | 
|---|
| [301] | 867 | return ('FAIL', "Non-numerics in apparent IPv4 reverse zone name") if $tmpzone !~ /^(?:\d+-)?[\d\.]+$/; | 
|---|
| [298] | 868 |  | 
|---|
|  | 869 | # Snag the octet pieces | 
|---|
|  | 870 | my @octs = split /\./, $tmpzone; | 
|---|
|  | 871 |  | 
|---|
|  | 872 | # Map result of a range manipulation to a mask length change.  Cheaper than finding the 2-root of $octets[0]+1. | 
|---|
| [304] | 873 | # Note we will not support /31 blocks, mostly due to issues telling "24-31" -> .24/29 apart from | 
|---|
|  | 874 | # "24-31" -> .24/31", with a litte bit of "/31 is icky". | 
|---|
|  | 875 | my %maskmap = (  3 => 2,  7 => 3, 15 => 4, 31 => 5, 63 => 6, 127 => 7, | 
|---|
|  | 876 | 30 => 2, 29 => 3, 28 => 4, 27 => 5, 26 => 6,  25 => 7 | 
|---|
|  | 877 | ); | 
|---|
| [298] | 878 |  | 
|---|
|  | 879 | # Handle "range" blocks, eg, 80-83.168.192.in-addr.arpa (192.168.80.0/22) | 
|---|
|  | 880 | # Need to take the size of the range to offset the basic octet-based mask length, | 
|---|
|  | 881 | # and make sure the first number in the range gets used as the network address for the block | 
|---|
| [304] | 882 | # Alternate form:  The second number is actually the real netmask, not the end of the range. | 
|---|
| [298] | 883 | my $masklen = 0; | 
|---|
| [307] | 884 | if ($octs[0] =~ /^((\d+)-(\d+))$/) {        # take the range... | 
|---|
|  | 885 | if (24 < $3 && $3 < 31) { | 
|---|
| [304] | 886 | # we have a real netmask | 
|---|
| [307] | 887 | $masklen = -$maskmap{$3}; | 
|---|
| [304] | 888 | } else { | 
|---|
|  | 889 | # we have a range.  NB:  only real CIDR ranges are supported | 
|---|
| [307] | 890 | $masklen -= $maskmap{-(eval $1)};       # find the mask base... | 
|---|
| [304] | 891 | } | 
|---|
| [307] | 892 | $octs[0] = $2;    # set the base octet of the range... | 
|---|
| [265] | 893 | } | 
|---|
| [298] | 894 | @octs = reverse @octs;      # We can reverse the octet pieces now that we've extracted and munged any ranges | 
|---|
| [265] | 895 |  | 
|---|
| [304] | 896 | # arguably we should only allow sub-octet range/mask in-addr.arpa | 
|---|
|  | 897 | # specifications in the least significant octet, but the code is | 
|---|
|  | 898 | # simpler if we deal with sub-octet delegations at any level. | 
|---|
|  | 899 |  | 
|---|
| [298] | 900 | # Now we find the "true" mask with the aid of the "base" calculated above | 
|---|
|  | 901 | if ($#octs == 0) { | 
|---|
|  | 902 | $masklen += 8; | 
|---|
|  | 903 | $tmpcidr = "$octs[0].0.0.0/$masklen";     # really hope we don't see one of these very often. | 
|---|
|  | 904 | } elsif ($#octs == 1) { | 
|---|
|  | 905 | $masklen += 16; | 
|---|
|  | 906 | $tmpcidr = "$octs[0].$octs[1].0.0/$masklen"; | 
|---|
|  | 907 | } elsif ($#octs == 2) { | 
|---|
|  | 908 | $masklen += 24; | 
|---|
|  | 909 | $tmpcidr = "$octs[0].$octs[1].$octs[2].0/$masklen"; | 
|---|
| [265] | 910 | } else { | 
|---|
| [298] | 911 | $masklen += 32; | 
|---|
|  | 912 | $tmpcidr = "$octs[0].$octs[1].$octs[2].$octs[3]/$masklen"; | 
|---|
| [265] | 913 | } | 
|---|
|  | 914 |  | 
|---|
| [298] | 915 | } elsif ($zone =~ /\.ip6\.arpa$/) { | 
|---|
|  | 916 | # v6 revzone, formal zone name type | 
|---|
|  | 917 | my $tmpzone = $zone; | 
|---|
|  | 918 | $tmpzone =~ s/\.ip6\.arpa\.?//; | 
|---|
| [301] | 919 | ##fixme:  if-n-when we decide we can support sub-nibble v6 zone names, we'll need to change this segment | 
|---|
|  | 920 | return ('FAIL', "Non-hexadecimals in apparent IPv6 reverse zone name") if $tmpzone !~ /^[a-fA-F\d\.]+$/; | 
|---|
| [298] | 921 | my @quads = reverse(split(/\./, $tmpzone)); | 
|---|
|  | 922 | $warnmsg .= "Apparent sub-/64 IPv6 reverse zone\n" if $#quads > 15; | 
|---|
|  | 923 | my $nc; | 
|---|
|  | 924 | foreach (@quads) { | 
|---|
| [301] | 925 | $tmpcidr .= $_; | 
|---|
|  | 926 | $tmpcidr .= ":" if ++$nc % 4 == 0; | 
|---|
| [298] | 927 | } | 
|---|
|  | 928 | my $nq = 1 if $nc % 4 != 0; | 
|---|
|  | 929 | my $mask = $nc * 4; # need to do this here because we probably increment it below | 
|---|
|  | 930 | while ($nc++ % 4 != 0) { | 
|---|
| [301] | 931 | $tmpcidr .= "0"; | 
|---|
| [298] | 932 | } | 
|---|
| [301] | 933 | $tmpcidr .= ($nq ? '::' : ':')."/$mask"; | 
|---|
| [298] | 934 | } | 
|---|
| [301] | 935 |  | 
|---|
|  | 936 | # Just to be sure, use NetAddr::IP to validate.  Saves a lot of nasty regex watching for valid octet values. | 
|---|
|  | 937 | return ('FAIL', "Invalid zone $zone (apparent netblock $tmpcidr)") | 
|---|
|  | 938 | unless $cidr = NetAddr::IP->new($tmpcidr); | 
|---|
|  | 939 |  | 
|---|
|  | 940 | if ($warnmsg) { | 
|---|
|  | 941 | $errstr = $warnmsg; | 
|---|
|  | 942 | return ('WARN', $cidr); | 
|---|
|  | 943 | } | 
|---|
|  | 944 | return ('OK', $cidr); | 
|---|
| [298] | 945 | } # done _zone2cidr() | 
|---|
| [265] | 946 |  | 
|---|
| [298] | 947 |  | 
|---|
| [228] | 948 | ## | 
|---|
| [2] | 949 | ## Initialization and cleanup subs | 
|---|
|  | 950 | ## | 
|---|
|  | 951 |  | 
|---|
| [55] | 952 |  | 
|---|
| [128] | 953 | ## DNSDB::loadConfig() | 
|---|
|  | 954 | # Load the minimum required initial state (DB connect info) from a config file | 
|---|
|  | 955 | # Load misc other bits while we're at it. | 
|---|
|  | 956 | # Takes an optional basename and config path to look for | 
|---|
|  | 957 | # Populates the %config and %def hashes | 
|---|
|  | 958 | sub loadConfig { | 
|---|
|  | 959 | my $basename = shift || '';   # this will work OK | 
|---|
| [218] | 960 | ##fixme  $basename isn't doing what I think I thought I was trying to do. | 
|---|
| [128] | 961 |  | 
|---|
|  | 962 | my $deferr = '';      # place to put error from default config file in case we can't find either one | 
|---|
|  | 963 |  | 
|---|
| [219] | 964 | my $configroot = "/etc/dnsdb";        ##CFG_LEAF## | 
|---|
| [128] | 965 | $configroot = '' if $basename =~ m|^/|; | 
|---|
|  | 966 | $basename .= ".conf" if $basename !~ /\.conf$/; | 
|---|
|  | 967 | my $defconfig = "$configroot/dnsdb.conf"; | 
|---|
|  | 968 | my $siteconfig = "$configroot/$basename"; | 
|---|
|  | 969 |  | 
|---|
|  | 970 | # System defaults | 
|---|
| [131] | 971 | __cfgload("$defconfig") or $deferr = $errstr; | 
|---|
| [128] | 972 |  | 
|---|
| [131] | 973 | # Per-site-ish settings. | 
|---|
|  | 974 | if ($basename ne '.conf') { | 
|---|
|  | 975 | unless (__cfgload("$siteconfig")) { | 
|---|
|  | 976 | $errstr = ($deferr ? "Error opening default config file $defconfig: $deferr\n" : ''). | 
|---|
| [128] | 977 | "Error opening site config file $siteconfig"; | 
|---|
| [131] | 978 | return; | 
|---|
|  | 979 | } | 
|---|
| [128] | 980 | } | 
|---|
|  | 981 |  | 
|---|
| [195] | 982 | # Munge log_failures. | 
|---|
|  | 983 | if ($config{log_failures} ne '1' && $config{log_failures} ne '0') { | 
|---|
|  | 984 | # true/false, on/off, yes/no all valid. | 
|---|
|  | 985 | if ($config{log_failures} =~ /^(?:true|false|on|off|yes|no)$/) { | 
|---|
|  | 986 | if ($config{log_failures} =~ /(?:true|on|yes)/) { | 
|---|
|  | 987 | $config{log_failures} = 1; | 
|---|
|  | 988 | } else { | 
|---|
|  | 989 | $config{log_failures} = 0; | 
|---|
|  | 990 | } | 
|---|
|  | 991 | } else { | 
|---|
|  | 992 | $errstr = "Bad log_failures setting $config{log_failures}"; | 
|---|
|  | 993 | $config{log_failures} = 1; | 
|---|
|  | 994 | # Bad setting shouldn't be fatal. | 
|---|
|  | 995 | # return 2; | 
|---|
|  | 996 | } | 
|---|
|  | 997 | } | 
|---|
|  | 998 |  | 
|---|
| [128] | 999 | # All good, clear the error and go home. | 
|---|
|  | 1000 | $errstr = ''; | 
|---|
|  | 1001 | return 1; | 
|---|
|  | 1002 | } # end loadConfig() | 
|---|
|  | 1003 |  | 
|---|
|  | 1004 |  | 
|---|
|  | 1005 | ## DNSDB::__cfgload() | 
|---|
|  | 1006 | # Private sub to parse a config file and load it into %config | 
|---|
|  | 1007 | # Takes a file handle on an open config file | 
|---|
|  | 1008 | sub __cfgload { | 
|---|
|  | 1009 | $errstr = ''; | 
|---|
|  | 1010 | my $cfgfile = shift; | 
|---|
| [131] | 1011 |  | 
|---|
| [128] | 1012 | if (open CFG, "<$cfgfile") { | 
|---|
|  | 1013 | while (<CFG>) { | 
|---|
|  | 1014 | chomp; | 
|---|
|  | 1015 | s/^\s*//; | 
|---|
|  | 1016 | next if /^#/; | 
|---|
|  | 1017 | next if /^$/; | 
|---|
|  | 1018 | # hmm.  more complex bits in this file might require [heading] headers, maybe? | 
|---|
|  | 1019 | #    $mode = $1 if /^\[(a-z)+]/; | 
|---|
|  | 1020 | # DB connect info | 
|---|
|  | 1021 | $config{dbname}   = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
|  | 1022 | $config{dbuser}   = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
|  | 1023 | $config{dbpass}   = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
|  | 1024 | $config{dbhost}   = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
|  | 1025 | # SOA defaults | 
|---|
|  | 1026 | $def{contact}     = $1 if /^contact\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
|  | 1027 | $def{prins}       = $1 if /^prins\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
| [201] | 1028 | $def{soattl}      = $1 if /^soattl\s*=\s*(\d+)/i; | 
|---|
|  | 1029 | $def{refresh}     = $1 if /^refresh\s*=\s*(\d+)/i; | 
|---|
|  | 1030 | $def{retry}       = $1 if /^retry\s*=\s*(\d+)/i; | 
|---|
|  | 1031 | $def{expire}      = $1 if /^expire\s*=\s*(\d+)/i; | 
|---|
|  | 1032 | $def{minttl}      = $1 if /^minttl\s*=\s*(\d+)/i; | 
|---|
|  | 1033 | $def{ttl}         = $1 if /^ttl\s*=\s*(\d+)/i; | 
|---|
| [128] | 1034 | # Mail settings | 
|---|
|  | 1035 | $config{mailhost}         = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
| [198] | 1036 | $config{mailnotify}       = $1 if /^mailnotify\s*=\s*([a-z0-9_.\@-]+)/i; | 
|---|
|  | 1037 | $config{mailsender}       = $1 if /^mailsender\s*=\s*([a-z0-9_.\@-]+)/i; | 
|---|
| [128] | 1038 | $config{mailname}         = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i; | 
|---|
| [195] | 1039 | $config{orgname}          = $1 if /^orgname\s*=\s*([a-z0-9\s_.,'-]+)/i; | 
|---|
|  | 1040 | $config{domain}           = $1 if /^domain\s*=\s*([a-z0-9_.-]+)/i; | 
|---|
| [163] | 1041 | # session - note this is fed directly to CGI::Session | 
|---|
| [216] | 1042 | $config{timeout}          = $1 if /^[tT][iI][mM][eE][oO][uU][tT]\s*=\s*(\d+[smhdwMy]?)/; | 
|---|
|  | 1043 | $config{sessiondir}       = $1 if m{^sessiondir\s*=\s*([a-z0-9/_.-]+)}i; | 
|---|
| [201] | 1044 | # misc | 
|---|
| [195] | 1045 | $config{log_failures}     = $1 if /^log_failures\s*=\s*([a-z01]+)/i; | 
|---|
| [201] | 1046 | $config{perpage}          = $1 if /^perpage\s*=\s*(\d+)/i; | 
|---|
| [128] | 1047 | } | 
|---|
|  | 1048 | close CFG; | 
|---|
|  | 1049 | } else { | 
|---|
|  | 1050 | $errstr = $!; | 
|---|
|  | 1051 | return; | 
|---|
|  | 1052 | } | 
|---|
|  | 1053 | return 1; | 
|---|
|  | 1054 | } # end __cfgload() | 
|---|
|  | 1055 |  | 
|---|
|  | 1056 |  | 
|---|
| [2] | 1057 | ## DNSDB::connectDB() | 
|---|
|  | 1058 | # Creates connection to DNS database. | 
|---|
|  | 1059 | # Requires the database name, username, and password. | 
|---|
|  | 1060 | # Returns a handle to the db. | 
|---|
|  | 1061 | # Set up for a PostgreSQL db;  could be any transactional DBMS with the | 
|---|
|  | 1062 | # right changes. | 
|---|
|  | 1063 | sub connectDB { | 
|---|
|  | 1064 | $errstr = ''; | 
|---|
| [15] | 1065 | my $dbname = shift; | 
|---|
|  | 1066 | my $user = shift; | 
|---|
|  | 1067 | my $pass = shift; | 
|---|
| [2] | 1068 | my $dbh; | 
|---|
|  | 1069 | my $DSN = "DBI:Pg:dbname=$dbname"; | 
|---|
|  | 1070 |  | 
|---|
|  | 1071 | my $host = shift; | 
|---|
|  | 1072 | $DSN .= ";host=$host" if $host; | 
|---|
|  | 1073 |  | 
|---|
|  | 1074 | # Note that we want to autocommit by default, and we will turn it off locally as necessary. | 
|---|
|  | 1075 | # We may not want to print gobbledygook errors;  YMMV.  Have to ponder that further. | 
|---|
|  | 1076 | $dbh = DBI->connect($DSN, $user, $pass, { | 
|---|
|  | 1077 | AutoCommit => 1, | 
|---|
|  | 1078 | PrintError => 0 | 
|---|
|  | 1079 | }) | 
|---|
|  | 1080 | or return (undef, $DBI::errstr) if(!$dbh); | 
|---|
|  | 1081 |  | 
|---|
| [212] | 1082 | ##fixme:  initialize the DB if we can't find the table (since, by definition, there's | 
|---|
|  | 1083 | # nothing there if we can't select from it...) | 
|---|
|  | 1084 | my $tblsth = $dbh->prepare("SELECT count(*) FROM pg_catalog.pg_class WHERE relkind='r' AND relname=?"); | 
|---|
|  | 1085 | my ($tblcount) = $dbh->selectrow_array($tblsth, undef, ('misc')); | 
|---|
|  | 1086 | return (undef,$DBI::errstr) if $dbh->err; | 
|---|
|  | 1087 |  | 
|---|
|  | 1088 | #if ($tblcount == 0) { | 
|---|
|  | 1089 | #  # create tables one at a time, checking for each. | 
|---|
|  | 1090 | #  return (undef, "check table misc missing"); | 
|---|
|  | 1091 | #} | 
|---|
|  | 1092 |  | 
|---|
|  | 1093 |  | 
|---|
|  | 1094 | # Return here if we can't select. | 
|---|
|  | 1095 | # This should retrieve the dbversion key. | 
|---|
|  | 1096 | my $sth = $dbh->prepare("SELECT key,value FROM misc WHERE misc_id=1"); | 
|---|
| [2] | 1097 | $sth->execute(); | 
|---|
|  | 1098 | return (undef,$DBI::errstr) if ($sth->err); | 
|---|
|  | 1099 |  | 
|---|
| [212] | 1100 | ##fixme:  do stuff to the DB on version mismatch | 
|---|
|  | 1101 | # x.y series should upgrade on $DNSDB::VERSION > misc(key=>version) | 
|---|
|  | 1102 | # DB should be downward-compatible;  column defaults should give sane (if possibly | 
|---|
|  | 1103 | # useless-and-needs-help) values in columns an older software stack doesn't know about. | 
|---|
|  | 1104 |  | 
|---|
| [2] | 1105 | # See if the select returned anything (or null data).  This should | 
|---|
|  | 1106 | # succeed if the select executed, but... | 
|---|
|  | 1107 | $sth->fetchrow(); | 
|---|
|  | 1108 | return (undef,$DBI::errstr)  if ($sth->err); | 
|---|
|  | 1109 |  | 
|---|
|  | 1110 | $sth->finish; | 
|---|
|  | 1111 |  | 
|---|
|  | 1112 | # If we get here, we should be OK. | 
|---|
|  | 1113 | return ($dbh,"DB connection OK"); | 
|---|
|  | 1114 | } # end connectDB | 
|---|
|  | 1115 |  | 
|---|
|  | 1116 |  | 
|---|
|  | 1117 | ## DNSDB::finish() | 
|---|
|  | 1118 | # Cleans up after database handles and so on. | 
|---|
|  | 1119 | # Requires a database handle | 
|---|
|  | 1120 | sub finish { | 
|---|
|  | 1121 | my $dbh = $_[0]; | 
|---|
|  | 1122 | $dbh->disconnect; | 
|---|
|  | 1123 | } # end finish | 
|---|
|  | 1124 |  | 
|---|
|  | 1125 |  | 
|---|
|  | 1126 | ## DNSDB::initGlobals() | 
|---|
|  | 1127 | # Initialize global variables | 
|---|
|  | 1128 | # NB: this does NOT include web-specific session variables! | 
|---|
|  | 1129 | # Requires a database handle | 
|---|
|  | 1130 | sub initGlobals { | 
|---|
|  | 1131 | my $dbh = shift; | 
|---|
|  | 1132 |  | 
|---|
| [208] | 1133 | # load record types from database | 
|---|
| [228] | 1134 | my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes"); | 
|---|
| [2] | 1135 | $sth->execute; | 
|---|
| [228] | 1136 | while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) { | 
|---|
| [2] | 1137 | $typemap{$recval} = $recname; | 
|---|
|  | 1138 | $reverse_typemap{$recname} = $recval; | 
|---|
| [228] | 1139 | # now we fill the record validation function hash | 
|---|
|  | 1140 | if ($stdflag < 5) { | 
|---|
|  | 1141 | my $fn = "_validate_$recval"; | 
|---|
|  | 1142 | $validators{$recval} = \&$fn; | 
|---|
|  | 1143 | } else { | 
|---|
|  | 1144 | my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }"; | 
|---|
|  | 1145 | $validators{$recval} = eval $fn; | 
|---|
|  | 1146 | } | 
|---|
| [2] | 1147 | } | 
|---|
|  | 1148 | } # end initGlobals | 
|---|
|  | 1149 |  | 
|---|
|  | 1150 |  | 
|---|
| [278] | 1151 | ## DNSDB::login() | 
|---|
|  | 1152 | # Takes a database handle, username and password | 
|---|
| [316] | 1153 | # Returns a userdata hash (UID, GID, username, fullname parts) if username exists, | 
|---|
|  | 1154 | # password matches the one on file, and account is not disabled | 
|---|
| [278] | 1155 | # Returns undef otherwise | 
|---|
|  | 1156 | sub login { | 
|---|
|  | 1157 | my $dbh = shift; | 
|---|
|  | 1158 | my $user = shift; | 
|---|
|  | 1159 | my $pass = shift; | 
|---|
|  | 1160 |  | 
|---|
| [316] | 1161 | my $userinfo = $dbh->selectrow_hashref("SELECT user_id,group_id,password,firstname,lastname,status". | 
|---|
|  | 1162 | " FROM users WHERE username=?", | 
|---|
| [279] | 1163 | undef, ($user) ); | 
|---|
|  | 1164 | return if !$userinfo; | 
|---|
| [316] | 1165 | return if !$userinfo->{status}; | 
|---|
| [278] | 1166 |  | 
|---|
| [279] | 1167 | if ($userinfo->{password} =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) { | 
|---|
| [278] | 1168 | # native passwords (crypt-md5) | 
|---|
| [279] | 1169 | return if $userinfo->{password} ne unix_md5_crypt($pass,$1); | 
|---|
|  | 1170 | } elsif ($userinfo->{password} =~ /^[0-9a-f]{32}$/) { | 
|---|
| [278] | 1171 | # VegaDNS import (hex-coded MD5) | 
|---|
| [279] | 1172 | return if $userinfo->{password} ne md5_hex($pass); | 
|---|
| [278] | 1173 | } else { | 
|---|
|  | 1174 | # plaintext (convenient now and then) | 
|---|
| [279] | 1175 | return if $userinfo->{password} ne $pass; | 
|---|
| [278] | 1176 | } | 
|---|
|  | 1177 |  | 
|---|
| [279] | 1178 | return $userinfo; | 
|---|
| [278] | 1179 | } # end login() | 
|---|
|  | 1180 |  | 
|---|
|  | 1181 |  | 
|---|
| [279] | 1182 | ## DNSDB::initActionLog() | 
|---|
|  | 1183 | # Set up action logging.  Takes a database handle and user ID | 
|---|
|  | 1184 | # Sets some internal globals and Does The Right Thing to set up a logging channel. | 
|---|
|  | 1185 | # This sets up _log() to spew out log entries to the defined channel without worrying | 
|---|
|  | 1186 | # about having to open a file or a syslog channel | 
|---|
|  | 1187 | ##fixme Need to call _initActionLog_blah() for various logging channels, configured | 
|---|
|  | 1188 | # via dnsdb.conf, in $config{log_channel} or something | 
|---|
|  | 1189 | # See https://secure.deepnet.cx/trac/dnsadmin/ticket/21 | 
|---|
|  | 1190 | sub initActionLog { | 
|---|
|  | 1191 | my $dbh = shift; | 
|---|
|  | 1192 | my $uid = shift; | 
|---|
|  | 1193 |  | 
|---|
|  | 1194 | return if !$uid; | 
|---|
|  | 1195 |  | 
|---|
|  | 1196 | # snag user info for logging.  there's got to be a way to not have to pass this back | 
|---|
|  | 1197 | # and forth from a caller, but web usage means no persistence we can rely on from | 
|---|
|  | 1198 | # the server side. | 
|---|
|  | 1199 | my ($username,$fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname". | 
|---|
|  | 1200 | " FROM users WHERE user_id=?", undef, ($uid)); | 
|---|
|  | 1201 | ##fixme: errors are unpossible! | 
|---|
|  | 1202 |  | 
|---|
|  | 1203 | $userdata{username} = $username; | 
|---|
|  | 1204 | $userdata{userid} = $uid; | 
|---|
|  | 1205 | $userdata{fullname} = $fullname; | 
|---|
|  | 1206 |  | 
|---|
|  | 1207 | # convert to real check once we have other logging channels | 
|---|
|  | 1208 | # if ($config{log_channel} eq 'sql') { | 
|---|
|  | 1209 | #   Open Log, Sez Me! | 
|---|
|  | 1210 | # } | 
|---|
|  | 1211 |  | 
|---|
|  | 1212 | } # end initActionLog | 
|---|
|  | 1213 |  | 
|---|
|  | 1214 |  | 
|---|
| [65] | 1215 | ## DNSDB::initPermissions() | 
|---|
|  | 1216 | # Set up permissions global | 
|---|
|  | 1217 | # Takes database handle and UID | 
|---|
|  | 1218 | sub initPermissions { | 
|---|
|  | 1219 | my $dbh = shift; | 
|---|
|  | 1220 | my $uid = shift; | 
|---|
|  | 1221 |  | 
|---|
|  | 1222 | #  %permissions = $(getPermissions($dbh,'user',$uid)); | 
|---|
|  | 1223 | getPermissions($dbh, 'user', $uid, \%permissions); | 
|---|
|  | 1224 |  | 
|---|
|  | 1225 | } # end initPermissions() | 
|---|
|  | 1226 |  | 
|---|
|  | 1227 |  | 
|---|
|  | 1228 | ## DNSDB::getPermissions() | 
|---|
|  | 1229 | # Get permissions from DB | 
|---|
|  | 1230 | # Requires DB handle, group or user flag, ID, and hashref. | 
|---|
|  | 1231 | sub getPermissions { | 
|---|
|  | 1232 | my $dbh = shift; | 
|---|
|  | 1233 | my $type = shift; | 
|---|
|  | 1234 | my $id = shift; | 
|---|
|  | 1235 | my $hash = shift; | 
|---|
|  | 1236 |  | 
|---|
|  | 1237 | my $sql = qq( | 
|---|
|  | 1238 | SELECT | 
|---|
|  | 1239 | p.admin,p.self_edit, | 
|---|
|  | 1240 | p.group_create,p.group_edit,p.group_delete, | 
|---|
|  | 1241 | p.user_create,p.user_edit,p.user_delete, | 
|---|
|  | 1242 | p.domain_create,p.domain_edit,p.domain_delete, | 
|---|
|  | 1243 | p.record_create,p.record_edit,p.record_delete | 
|---|
|  | 1244 | FROM permissions p | 
|---|
|  | 1245 | ); | 
|---|
|  | 1246 | if ($type eq 'group') { | 
|---|
|  | 1247 | $sql .= qq( | 
|---|
|  | 1248 | JOIN groups g ON g.permission_id=p.permission_id | 
|---|
|  | 1249 | WHERE g.group_id=? | 
|---|
|  | 1250 | ); | 
|---|
|  | 1251 | } else { | 
|---|
|  | 1252 | $sql .= qq( | 
|---|
|  | 1253 | JOIN users u ON u.permission_id=p.permission_id | 
|---|
|  | 1254 | WHERE u.user_id=? | 
|---|
|  | 1255 | ); | 
|---|
|  | 1256 | } | 
|---|
|  | 1257 |  | 
|---|
|  | 1258 | my $sth = $dbh->prepare($sql); | 
|---|
|  | 1259 |  | 
|---|
|  | 1260 | $sth->execute($id) or die "argh: ".$sth->errstr; | 
|---|
|  | 1261 |  | 
|---|
|  | 1262 | #  my $permref = $sth->fetchrow_hashref; | 
|---|
|  | 1263 | #  return $permref; | 
|---|
|  | 1264 | #  $hash = $permref; | 
|---|
|  | 1265 | # Eww.  Need to learn how to forcibly drop a hashref onto an existing hash. | 
|---|
|  | 1266 | ($hash->{admin},$hash->{self_edit}, | 
|---|
|  | 1267 | $hash->{group_create},$hash->{group_edit},$hash->{group_delete}, | 
|---|
|  | 1268 | $hash->{user_create},$hash->{user_edit},$hash->{user_delete}, | 
|---|
|  | 1269 | $hash->{domain_create},$hash->{domain_edit},$hash->{domain_delete}, | 
|---|
|  | 1270 | $hash->{record_create},$hash->{record_edit},$hash->{record_delete}) | 
|---|
|  | 1271 | = $sth->fetchrow_array; | 
|---|
|  | 1272 |  | 
|---|
|  | 1273 | } # end getPermissions() | 
|---|
|  | 1274 |  | 
|---|
|  | 1275 |  | 
|---|
|  | 1276 | ## DNSDB::changePermissions() | 
|---|
|  | 1277 | # Update an ACL entry | 
|---|
|  | 1278 | # Takes a db handle, type, owner-id, and hashref for the changed permissions. | 
|---|
|  | 1279 | sub changePermissions { | 
|---|
|  | 1280 | my $dbh = shift; | 
|---|
|  | 1281 | my $type = shift; | 
|---|
|  | 1282 | my $id = shift; | 
|---|
|  | 1283 | my $newperms = shift; | 
|---|
| [87] | 1284 | my $inherit = shift || 0; | 
|---|
| [65] | 1285 |  | 
|---|
| [294] | 1286 | my $resultmsg = ''; | 
|---|
| [66] | 1287 |  | 
|---|
| [87] | 1288 | # see if we're switching from inherited to custom.  for bonus points, | 
|---|
|  | 1289 | # snag the permid and parent permid anyway, since we'll need the permid | 
|---|
|  | 1290 | # to set/alter custom perms, and both if we're switching from custom to | 
|---|
|  | 1291 | # inherited. | 
|---|
| [294] | 1292 | my $sth = $dbh->prepare("SELECT (u.permission_id=g.permission_id) AS was_inherited,u.permission_id,g.permission_id,". | 
|---|
|  | 1293 | ($type eq 'user' ? 'u.group_id,u.username' : 'u.parent_group_id,u.group_name'). | 
|---|
| [65] | 1294 | " FROM ".($type eq 'user' ? 'users' : 'groups')." u ". | 
|---|
| [66] | 1295 | " JOIN groups g ON u.".($type eq 'user' ? '' : 'parent_')."group_id=g.group_id ". | 
|---|
| [65] | 1296 | " WHERE u.".($type eq 'user' ? 'user' : 'group')."_id=?"); | 
|---|
|  | 1297 | $sth->execute($id); | 
|---|
|  | 1298 |  | 
|---|
| [294] | 1299 | my ($wasinherited,$permid,$parpermid,$parid,$name) = $sth->fetchrow_array; | 
|---|
| [66] | 1300 |  | 
|---|
| [78] | 1301 | # hack phtoui | 
|---|
|  | 1302 | # group id 1 is "special" in that it's it's own parent (err...  possibly.) | 
|---|
|  | 1303 | # may make its parent id 0 which doesn't exist, and as a bonus is Perl-false. | 
|---|
|  | 1304 | $wasinherited = 0 if ($type eq 'group' && $id == 1); | 
|---|
|  | 1305 |  | 
|---|
| [66] | 1306 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 1307 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 1308 |  | 
|---|
|  | 1309 | # Wrap all the SQL in a transaction | 
|---|
|  | 1310 | eval { | 
|---|
| [87] | 1311 | if ($inherit) { | 
|---|
|  | 1312 |  | 
|---|
|  | 1313 | $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='t',permission_id=? ". | 
|---|
|  | 1314 | "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($parpermid, $id) ); | 
|---|
|  | 1315 | $dbh->do("DELETE FROM permissions WHERE permission_id=?", undef, ($permid) ); | 
|---|
|  | 1316 |  | 
|---|
|  | 1317 | } else { | 
|---|
|  | 1318 |  | 
|---|
|  | 1319 | if ($wasinherited) {      # munge new permission entry in if we're switching from inherited perms | 
|---|
| [66] | 1320 | ##fixme: need to add semirecursive bit to properly munge inherited permission ID on subgroups and users | 
|---|
| [87] | 1321 | # ... if'n'when we have groups with fully inherited permissions. | 
|---|
|  | 1322 | # SQL is coo | 
|---|
|  | 1323 | $dbh->do("INSERT INTO permissions ($permlist,".($type eq 'user' ? 'user' : 'group')."_id) ". | 
|---|
|  | 1324 | "SELECT $permlist,? FROM permissions WHERE permission_id=?", undef, ($id,$permid) ); | 
|---|
|  | 1325 | ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions ". | 
|---|
|  | 1326 | "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($id) ); | 
|---|
|  | 1327 | $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='f',permission_id=? ". | 
|---|
|  | 1328 | "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($permid, $id) ); | 
|---|
| [66] | 1329 | } | 
|---|
| [78] | 1330 |  | 
|---|
| [87] | 1331 | # and now set the permissions we were passed | 
|---|
|  | 1332 | foreach (@permtypes) { | 
|---|
|  | 1333 | if (defined ($newperms->{$_})) { | 
|---|
|  | 1334 | $dbh->do("UPDATE permissions SET $_=? WHERE permission_id=?", undef, ($newperms->{$_},$permid) ); | 
|---|
|  | 1335 | } | 
|---|
|  | 1336 | } | 
|---|
|  | 1337 |  | 
|---|
|  | 1338 | } # (inherited->)? custom | 
|---|
|  | 1339 |  | 
|---|
| [294] | 1340 | if ($type eq 'user') { | 
|---|
|  | 1341 | $resultmsg = "Updated permissions for user $name"; | 
|---|
|  | 1342 | } else { | 
|---|
|  | 1343 | $resultmsg = "Updated default permissions for group $name"; | 
|---|
|  | 1344 | } | 
|---|
|  | 1345 | _log($dbh, (group_id => ($type eq 'user' ? $parid : $id), entry => $resultmsg)); | 
|---|
| [66] | 1346 | $dbh->commit; | 
|---|
|  | 1347 | }; # end eval | 
|---|
|  | 1348 | if ($@) { | 
|---|
|  | 1349 | my $msg = $@; | 
|---|
|  | 1350 | eval { $dbh->rollback; }; | 
|---|
| [294] | 1351 | return ('FAIL',"Error changing permissions: $msg"); | 
|---|
| [66] | 1352 | } | 
|---|
|  | 1353 |  | 
|---|
| [294] | 1354 | return ('OK',$resultmsg); | 
|---|
| [65] | 1355 | } # end changePermissions() | 
|---|
|  | 1356 |  | 
|---|
|  | 1357 |  | 
|---|
| [67] | 1358 | ## DNSDB::comparePermissions() | 
|---|
|  | 1359 | # Compare two permission hashes | 
|---|
|  | 1360 | # Returns '>', '<', '=', '!' | 
|---|
|  | 1361 | sub comparePermissions { | 
|---|
|  | 1362 | my $p1 = shift; | 
|---|
|  | 1363 | my $p2 = shift; | 
|---|
|  | 1364 |  | 
|---|
|  | 1365 | my $retval = '=';     # assume equality until proven otherwise | 
|---|
|  | 1366 |  | 
|---|
|  | 1367 | no warnings "uninitialized"; | 
|---|
|  | 1368 |  | 
|---|
|  | 1369 | foreach (@permtypes) { | 
|---|
|  | 1370 | next if $p1->{$_} == $p2->{$_};     # equal is good | 
|---|
|  | 1371 | if ($p1->{$_} && !$p2->{$_}) { | 
|---|
|  | 1372 | if ($retval eq '<') {     # if we've already found an unequal pair where | 
|---|
|  | 1373 | $retval = '!';          # $p2 has more access, and we now find a pair | 
|---|
|  | 1374 | last;                   # where $p1 has more access, the overall access | 
|---|
|  | 1375 | }                         # is neither greater or lesser, it's unequal. | 
|---|
|  | 1376 | $retval = '>'; | 
|---|
|  | 1377 | } | 
|---|
|  | 1378 | if (!$p1->{$_} && $p2->{$_}) { | 
|---|
|  | 1379 | if ($retval eq '>') {     # if we've already found an unequal pair where | 
|---|
|  | 1380 | $retval = '!';          # $p1 has more access, and we now find a pair | 
|---|
|  | 1381 | last;                   # where $p2 has more access, the overall access | 
|---|
|  | 1382 | }                         # is neither greater or lesser, it's unequal. | 
|---|
|  | 1383 | $retval = '<'; | 
|---|
|  | 1384 | } | 
|---|
|  | 1385 | } | 
|---|
|  | 1386 | return $retval; | 
|---|
|  | 1387 | } # end comparePermissions() | 
|---|
|  | 1388 |  | 
|---|
|  | 1389 |  | 
|---|
| [112] | 1390 | ## DNSDB::changeGroup() | 
|---|
|  | 1391 | # Change group ID of an entity | 
|---|
|  | 1392 | # Takes a database handle, entity type, entity ID, and new group ID | 
|---|
|  | 1393 | sub changeGroup { | 
|---|
|  | 1394 | my $dbh = shift; | 
|---|
|  | 1395 | my $type = shift; | 
|---|
|  | 1396 | my $id = shift; | 
|---|
|  | 1397 | my $newgrp = shift; | 
|---|
|  | 1398 |  | 
|---|
|  | 1399 | ##fixme:  fail on not enough args | 
|---|
|  | 1400 | #return ('FAIL', "Missing | 
|---|
|  | 1401 |  | 
|---|
| [295] | 1402 | return ('FAIL', "Can't change the group of a $type") | 
|---|
|  | 1403 | unless grep /^$type$/, ('domain','revzone','user','group');     # could be extended for defrecs? | 
|---|
|  | 1404 |  | 
|---|
|  | 1405 | # Collect some names for logging and messages | 
|---|
|  | 1406 | my $entname; | 
|---|
| [112] | 1407 | if ($type eq 'domain') { | 
|---|
| [295] | 1408 | $entname = domainName($dbh, $id); | 
|---|
|  | 1409 | } elsif ($type eq 'revzone') { | 
|---|
|  | 1410 | $entname = revName($dbh, $id); | 
|---|
| [112] | 1411 | } elsif ($type eq 'user') { | 
|---|
| [295] | 1412 | $entname = userFullName($dbh, $id, '%u'); | 
|---|
| [112] | 1413 | } elsif ($type eq 'group') { | 
|---|
| [295] | 1414 | $entname = groupName($dbh, $id); | 
|---|
| [112] | 1415 | } | 
|---|
| [295] | 1416 |  | 
|---|
|  | 1417 | my ($oldgid) = $dbh->selectrow_array("SELECT group_id FROM $par_tbl{$type} WHERE $id_col{$type}=?", | 
|---|
|  | 1418 | undef, ($id)); | 
|---|
|  | 1419 | my $oldgname = groupName($dbh, $oldgid); | 
|---|
|  | 1420 | my $newgname = groupName($dbh, $newgrp); | 
|---|
|  | 1421 |  | 
|---|
|  | 1422 | return ('FAIL', "Can't move things into a group that doesn't exist") if !$newgname; | 
|---|
|  | 1423 |  | 
|---|
|  | 1424 | return ('WARN', "Nothing to do, new group is the same as the old group") if $oldgid == $newgrp; | 
|---|
|  | 1425 |  | 
|---|
|  | 1426 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 1427 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 1428 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 1429 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 1430 |  | 
|---|
|  | 1431 | eval { | 
|---|
|  | 1432 | $dbh->do("UPDATE $par_tbl{$type} SET group_id=? WHERE $id_col{$type}=?", undef, ($newgrp, $id)); | 
|---|
|  | 1433 | # Log the change in both the old and new groups | 
|---|
|  | 1434 | _log($dbh, (group_id => $oldgid, entry => "Moved $type $entname from $oldgname to $newgname")); | 
|---|
|  | 1435 | _log($dbh, (group_id => $newgrp, entry => "Moved $type $entname from $oldgname to $newgname")); | 
|---|
|  | 1436 | $dbh->commit; | 
|---|
|  | 1437 | }; | 
|---|
|  | 1438 | if ($@) { | 
|---|
|  | 1439 | my $msg = $@; | 
|---|
|  | 1440 | eval { $dbh->rollback; }; | 
|---|
|  | 1441 | if ($config{log_failures}) { | 
|---|
|  | 1442 | _log($dbh, (group_id => $oldgid, entry => "Error moving $type $entname to $newgname: $msg")); | 
|---|
|  | 1443 | $dbh->commit;     # since we enabled transactions earlier | 
|---|
|  | 1444 | } | 
|---|
|  | 1445 | return ('FAIL',"Error moving $type $entname to $newgname: $msg"); | 
|---|
|  | 1446 | } | 
|---|
|  | 1447 |  | 
|---|
|  | 1448 | return ('OK',"Moved $type $entname from $oldgname to $newgname"); | 
|---|
| [112] | 1449 | } # end changeGroup() | 
|---|
|  | 1450 |  | 
|---|
|  | 1451 |  | 
|---|
| [2] | 1452 | ## | 
|---|
|  | 1453 | ## Processing subs | 
|---|
|  | 1454 | ## | 
|---|
|  | 1455 |  | 
|---|
|  | 1456 | ## DNSDB::addDomain() | 
|---|
|  | 1457 | # Add a domain | 
|---|
| [190] | 1458 | # Takes a database handle, domain name, numeric group, boolean(ish) state (active/inactive), | 
|---|
|  | 1459 | # and user info hash (for logging). | 
|---|
| [2] | 1460 | # Returns a status code and message | 
|---|
|  | 1461 | sub addDomain { | 
|---|
|  | 1462 | $errstr = ''; | 
|---|
|  | 1463 | my $dbh = shift; | 
|---|
|  | 1464 | return ('FAIL',"Need database handle") if !$dbh; | 
|---|
|  | 1465 | my $domain = shift; | 
|---|
| [91] | 1466 | return ('FAIL',"Domain must not be blank") if !$domain; | 
|---|
| [2] | 1467 | my $group = shift; | 
|---|
|  | 1468 | return ('FAIL',"Need group") if !defined($group); | 
|---|
|  | 1469 | my $state = shift; | 
|---|
|  | 1470 | return ('FAIL',"Need domain status") if !defined($state); | 
|---|
|  | 1471 |  | 
|---|
| [116] | 1472 | $state = 1 if $state =~ /^active$/; | 
|---|
|  | 1473 | $state = 1 if $state =~ /^on$/; | 
|---|
|  | 1474 | $state = 0 if $state =~ /^inactive$/; | 
|---|
|  | 1475 | $state = 0 if $state =~ /^off$/; | 
|---|
|  | 1476 |  | 
|---|
|  | 1477 | return ('FAIL',"Invalid domain status") if $state !~ /^\d+$/; | 
|---|
|  | 1478 |  | 
|---|
| [190] | 1479 | return ('FAIL', "Invalid characters in domain") if $domain !~ /^[a-zA-Z0-9_.-]+$/; | 
|---|
|  | 1480 |  | 
|---|
| [38] | 1481 | my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?"); | 
|---|
| [3] | 1482 | my $dom_id; | 
|---|
|  | 1483 |  | 
|---|
| [38] | 1484 | # quick check to start to see if we've already got one | 
|---|
|  | 1485 | $sth->execute($domain); | 
|---|
|  | 1486 | ($dom_id) = $sth->fetchrow_array; | 
|---|
|  | 1487 |  | 
|---|
|  | 1488 | return ('FAIL', "Domain already exists") if $dom_id; | 
|---|
|  | 1489 |  | 
|---|
| [2] | 1490 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 1491 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 1492 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 1493 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 1494 |  | 
|---|
|  | 1495 | # Wrap all the SQL in a transaction | 
|---|
|  | 1496 | eval { | 
|---|
|  | 1497 | # insert the domain... | 
|---|
| [190] | 1498 | $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($domain, $group, $state)); | 
|---|
| [2] | 1499 |  | 
|---|
|  | 1500 | # get the ID... | 
|---|
| [190] | 1501 | ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain)); | 
|---|
| [2] | 1502 |  | 
|---|
| [284] | 1503 | _log($dbh, (domain_id => $dom_id, group_id => $group, | 
|---|
| [259] | 1504 | entry => "Added ".($state ? 'active' : 'inactive')." domain $domain")); | 
|---|
| [190] | 1505 |  | 
|---|
| [2] | 1506 | # ... and now we construct the standard records from the default set.  NB:  group should be variable. | 
|---|
| [190] | 1507 | my $sth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?"); | 
|---|
|  | 1508 | my $sth_in = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,distance,weight,port,ttl)". | 
|---|
|  | 1509 | " VALUES ($dom_id,?,?,?,?,?,?,?)"); | 
|---|
|  | 1510 | $sth->execute($group); | 
|---|
| [3] | 1511 | while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $sth->fetchrow_array()) { | 
|---|
| [2] | 1512 | $host =~ s/DOMAIN/$domain/g; | 
|---|
| [37] | 1513 | $val =~ s/DOMAIN/$domain/g; | 
|---|
| [3] | 1514 | $sth_in->execute($host,$type,$val,$dist,$weight,$port,$ttl); | 
|---|
| [190] | 1515 | if ($typemap{$type} eq 'SOA') { | 
|---|
|  | 1516 | my @tmp1 = split /:/, $host; | 
|---|
|  | 1517 | my @tmp2 = split /:/, $val; | 
|---|
| [284] | 1518 | _log($dbh, (domain_id => $dom_id, group_id => $group, | 
|---|
|  | 1519 | entry => "[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". | 
|---|
| [257] | 1520 | "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); | 
|---|
| [190] | 1521 | } else { | 
|---|
|  | 1522 | my $logentry = "[new $domain] Added record '$host $typemap{$type}"; | 
|---|
|  | 1523 | $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX'; | 
|---|
|  | 1524 | $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV'; | 
|---|
| [284] | 1525 | _log($dbh, (domain_id => $dom_id, group_id => $group, | 
|---|
|  | 1526 | entry => $logentry." $val', TTL $ttl")); | 
|---|
| [190] | 1527 | } | 
|---|
| [2] | 1528 | } | 
|---|
|  | 1529 |  | 
|---|
|  | 1530 | # once we get here, we should have suceeded. | 
|---|
|  | 1531 | $dbh->commit; | 
|---|
|  | 1532 | }; # end eval | 
|---|
|  | 1533 |  | 
|---|
|  | 1534 | if ($@) { | 
|---|
|  | 1535 | my $msg = $@; | 
|---|
|  | 1536 | eval { $dbh->rollback; }; | 
|---|
| [286] | 1537 | _log($dbh, (group_id => $group, entry => "Failed adding domain $domain ($msg)")) | 
|---|
| [284] | 1538 | if $config{log_failures}; | 
|---|
|  | 1539 | $dbh->commit;       # since we enabled transactions earlier | 
|---|
| [193] | 1540 | return ('FAIL',$msg); | 
|---|
| [2] | 1541 | } else { | 
|---|
| [3] | 1542 | return ('OK',$dom_id); | 
|---|
| [2] | 1543 | } | 
|---|
|  | 1544 | } # end addDomain | 
|---|
|  | 1545 |  | 
|---|
|  | 1546 |  | 
|---|
| [274] | 1547 | ## DNSDB::delZone() | 
|---|
|  | 1548 | # Delete a forward or reverse zone. | 
|---|
|  | 1549 | # Takes a database handle, zone ID, and forward/reverse flag. | 
|---|
| [3] | 1550 | # for now, just delete the records, then the domain. | 
|---|
|  | 1551 | # later we may want to archive it in some way instead (status code 2, for example?) | 
|---|
| [274] | 1552 | sub delZone { | 
|---|
| [3] | 1553 | my $dbh = shift; | 
|---|
| [274] | 1554 | my $zoneid = shift; | 
|---|
|  | 1555 | my $revrec = shift; | 
|---|
| [3] | 1556 |  | 
|---|
|  | 1557 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 1558 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 1559 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 1560 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 1561 |  | 
|---|
| [285] | 1562 | my $msg = ''; | 
|---|
| [23] | 1563 | my $failmsg = ''; | 
|---|
| [285] | 1564 | my $zone = ($revrec eq 'n' ? domainName($dbh, $zoneid) : revName($dbh, $zoneid)); | 
|---|
| [23] | 1565 |  | 
|---|
| [285] | 1566 | # Set this up here since we may use if if $config{log_failures} is enabled | 
|---|
|  | 1567 | my %loghash; | 
|---|
|  | 1568 | $loghash{domain_id} = $zoneid if $revrec eq 'n'; | 
|---|
|  | 1569 | $loghash{rdns_id} = $zoneid if $revrec eq 'y'; | 
|---|
|  | 1570 | $loghash{group_id} = parentID($dbh, | 
|---|
|  | 1571 | (id => $zoneid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ); | 
|---|
|  | 1572 |  | 
|---|
| [3] | 1573 | # Wrap all the SQL in a transaction | 
|---|
|  | 1574 | eval { | 
|---|
| [274] | 1575 | # Disentangle custom record types before removing the | 
|---|
|  | 1576 | # ones that are only in the zone to be deleted | 
|---|
|  | 1577 | if ($revrec eq 'n') { | 
|---|
|  | 1578 | my $sth = $dbh->prepare("UPDATE records SET type=?,domain_id=0 WHERE domain_id=? AND type=?"); | 
|---|
|  | 1579 | $failmsg = "Failure converting multizone types to single-zone"; | 
|---|
|  | 1580 | $sth->execute($reverse_typemap{PTR}, $zoneid, 65280); | 
|---|
|  | 1581 | $sth->execute($reverse_typemap{PTR}, $zoneid, 65281); | 
|---|
|  | 1582 | $sth->execute(65282, $zoneid, 65283); | 
|---|
|  | 1583 | $sth->execute(65282, $zoneid, 65284); | 
|---|
|  | 1584 | $failmsg = "Failure removing domain records"; | 
|---|
|  | 1585 | $dbh->do("DELETE FROM records WHERE domain_id=?", undef, ($zoneid)); | 
|---|
|  | 1586 | $failmsg = "Failure removing domain"; | 
|---|
|  | 1587 | $dbh->do("DELETE FROM domains WHERE domain_id=?", undef, ($zoneid)); | 
|---|
|  | 1588 | } else { | 
|---|
|  | 1589 | my $sth = $dbh->prepare("UPDATE records SET type=?,rdns_id=0 WHERE rdns_id=? AND type=?"); | 
|---|
|  | 1590 | $failmsg = "Failure converting multizone types to single-zone"; | 
|---|
|  | 1591 | $sth->execute($reverse_typemap{A}, $zoneid, 65280); | 
|---|
|  | 1592 | $sth->execute($reverse_typemap{AAAA}, $zoneid, 65281); | 
|---|
|  | 1593 | # We don't have an "A template" or "AAAA template" type, although it might be useful for symmetry. | 
|---|
|  | 1594 | #      $sth->execute(65285?, $zoneid, 65283); | 
|---|
|  | 1595 | #      $sth->execute(65285?, $zoneid, 65284); | 
|---|
|  | 1596 | $failmsg = "Failure removing reverse records"; | 
|---|
|  | 1597 | $dbh->do("DELETE FROM records WHERE rdns_id=?", undef, ($zoneid)); | 
|---|
|  | 1598 | $failmsg = "Failure removing reverse zone"; | 
|---|
|  | 1599 | $dbh->do("DELETE FROM revzones WHERE rdns_id=?", undef, ($zoneid)); | 
|---|
|  | 1600 | } | 
|---|
| [3] | 1601 |  | 
|---|
| [285] | 1602 | $msg = "Deleted ".($revrec eq 'n' ? 'domain' : 'reverse zone')." $zone"; | 
|---|
|  | 1603 | $loghash{entry} = $msg; | 
|---|
|  | 1604 | _log($dbh, %loghash); | 
|---|
|  | 1605 |  | 
|---|
| [3] | 1606 | # once we get here, we should have suceeded. | 
|---|
| [23] | 1607 | $dbh->commit; | 
|---|
| [3] | 1608 | }; # end eval | 
|---|
|  | 1609 |  | 
|---|
|  | 1610 | if ($@) { | 
|---|
| [285] | 1611 | $msg = $@; | 
|---|
| [3] | 1612 | eval { $dbh->rollback; }; | 
|---|
| [295] | 1613 | $loghash{entry} = "Error deleting $zone: $msg ($failmsg)"; | 
|---|
|  | 1614 | if ($config{log_failures}) { | 
|---|
|  | 1615 | _log($dbh, %loghash); | 
|---|
|  | 1616 | $dbh->commit;     # since we enabled transactions earlier | 
|---|
|  | 1617 | } | 
|---|
|  | 1618 | return ('FAIL', $loghash{entry}); | 
|---|
| [3] | 1619 | } else { | 
|---|
| [295] | 1620 | return ('OK', $msg); | 
|---|
| [3] | 1621 | } | 
|---|
|  | 1622 |  | 
|---|
| [274] | 1623 | } # end delZone() | 
|---|
| [3] | 1624 |  | 
|---|
|  | 1625 |  | 
|---|
| [2] | 1626 | ## DNSDB::domainName() | 
|---|
|  | 1627 | # Return the domain name based on a domain ID | 
|---|
|  | 1628 | # Takes a database handle and the domain ID | 
|---|
|  | 1629 | # Returns the domain name or undef on failure | 
|---|
|  | 1630 | sub domainName { | 
|---|
|  | 1631 | $errstr = ''; | 
|---|
|  | 1632 | my $dbh = shift; | 
|---|
|  | 1633 | my $domid = shift; | 
|---|
| [91] | 1634 | my ($domname) = $dbh->selectrow_array("SELECT domain FROM domains WHERE domain_id=?", undef, ($domid) ); | 
|---|
| [2] | 1635 | $errstr = $DBI::errstr if !$domname; | 
|---|
|  | 1636 | return $domname if $domname; | 
|---|
| [91] | 1637 | } # end domainName() | 
|---|
| [2] | 1638 |  | 
|---|
|  | 1639 |  | 
|---|
| [224] | 1640 | ## DNSDB::revName() | 
|---|
|  | 1641 | # Return the reverse zone name based on an rDNS ID | 
|---|
|  | 1642 | # Takes a database handle and the rDNS ID | 
|---|
|  | 1643 | # Returns the reverse zone name or undef on failure | 
|---|
|  | 1644 | sub revName { | 
|---|
|  | 1645 | $errstr = ''; | 
|---|
|  | 1646 | my $dbh = shift; | 
|---|
|  | 1647 | my $revid = shift; | 
|---|
|  | 1648 | my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) ); | 
|---|
|  | 1649 | $errstr = $DBI::errstr if !$revname; | 
|---|
|  | 1650 | return $revname if $revname; | 
|---|
|  | 1651 | } # end revName() | 
|---|
|  | 1652 |  | 
|---|
|  | 1653 |  | 
|---|
| [91] | 1654 | ## DNSDB::domainID() | 
|---|
|  | 1655 | # Takes a database handle and domain name | 
|---|
|  | 1656 | # Returns the domain ID number | 
|---|
|  | 1657 | sub domainID { | 
|---|
|  | 1658 | $errstr = ''; | 
|---|
|  | 1659 | my $dbh = shift; | 
|---|
|  | 1660 | my $domain = shift; | 
|---|
|  | 1661 | my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain) ); | 
|---|
|  | 1662 | $errstr = $DBI::errstr if !$domid; | 
|---|
|  | 1663 | return $domid if $domid; | 
|---|
|  | 1664 | } # end domainID() | 
|---|
|  | 1665 |  | 
|---|
|  | 1666 |  | 
|---|
| [276] | 1667 | ## DNSDB::revID() | 
|---|
|  | 1668 | # Takes a database handle and reverse zone name | 
|---|
|  | 1669 | # Returns the rDNS ID number | 
|---|
|  | 1670 | sub revID { | 
|---|
|  | 1671 | $errstr = ''; | 
|---|
|  | 1672 | my $dbh = shift; | 
|---|
|  | 1673 | my $revzone = shift; | 
|---|
|  | 1674 | my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ($revzone) ); | 
|---|
|  | 1675 | $errstr = $DBI::errstr if !$revid; | 
|---|
|  | 1676 | return $revid if $revid; | 
|---|
|  | 1677 | } # end revID() | 
|---|
|  | 1678 |  | 
|---|
|  | 1679 |  | 
|---|
| [260] | 1680 | ## DNSDB::addRDNS | 
|---|
|  | 1681 | # Adds a reverse DNS zone | 
|---|
| [286] | 1682 | # Takes a database handle, CIDR block, reverse DNS pattern, numeric group, | 
|---|
|  | 1683 | # and boolean(ish) state (active/inactive) | 
|---|
| [260] | 1684 | # Returns a status code and message | 
|---|
|  | 1685 | sub addRDNS { | 
|---|
|  | 1686 | my $dbh = shift; | 
|---|
|  | 1687 | my $zone = NetAddr::IP->new(shift); | 
|---|
|  | 1688 | return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/); | 
|---|
| [270] | 1689 | my $revpatt = shift;  # construct a custom (A/AAAA+)? PTR template record | 
|---|
| [260] | 1690 | my $group = shift; | 
|---|
|  | 1691 | my $state = shift; | 
|---|
|  | 1692 |  | 
|---|
|  | 1693 | $state = 1 if $state =~ /^active$/; | 
|---|
|  | 1694 | $state = 1 if $state =~ /^on$/; | 
|---|
|  | 1695 | $state = 0 if $state =~ /^inactive$/; | 
|---|
|  | 1696 | $state = 0 if $state =~ /^off$/; | 
|---|
|  | 1697 |  | 
|---|
|  | 1698 | return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/; | 
|---|
|  | 1699 |  | 
|---|
|  | 1700 | # quick check to start to see if we've already got one | 
|---|
| [299] | 1701 | my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ("$zone")); | 
|---|
| [260] | 1702 |  | 
|---|
|  | 1703 | return ('FAIL', "Zone already exists") if $rdns_id; | 
|---|
|  | 1704 |  | 
|---|
|  | 1705 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 1706 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 1707 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 1708 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 1709 |  | 
|---|
| [264] | 1710 | my $warnstr = ''; | 
|---|
| [270] | 1711 | my $defttl = 3600;    # 1 hour should be reasonable.  And unless things have gone horribly | 
|---|
|  | 1712 | # wrong, we should have a value to override this anyway. | 
|---|
| [264] | 1713 |  | 
|---|
| [260] | 1714 | # Wrap all the SQL in a transaction | 
|---|
|  | 1715 | eval { | 
|---|
|  | 1716 | # insert the domain... | 
|---|
|  | 1717 | $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($zone, $group, $state)); | 
|---|
|  | 1718 |  | 
|---|
|  | 1719 | # get the ID... | 
|---|
|  | 1720 | ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); | 
|---|
|  | 1721 |  | 
|---|
| [286] | 1722 | _log($dbh, (rdns_id => $rdns_id, group_id => $group, | 
|---|
| [260] | 1723 | entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone")); | 
|---|
|  | 1724 |  | 
|---|
|  | 1725 | # ... and now we construct the standard records from the default set.  NB:  group should be variable. | 
|---|
|  | 1726 | my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?"); | 
|---|
| [269] | 1727 | my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,domain_id,host,type,val,ttl)". | 
|---|
|  | 1728 | " VALUES ($rdns_id,?,?,?,?,?)"); | 
|---|
| [260] | 1729 | $sth->execute($group); | 
|---|
|  | 1730 | while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) { | 
|---|
| [264] | 1731 | # Silently skip v4/v6 mismatches.  This is not an error, this is expected. | 
|---|
|  | 1732 | if ($zone->{isv6}) { | 
|---|
|  | 1733 | next if ($type == 65280 || $type == 65283); | 
|---|
|  | 1734 | } else { | 
|---|
|  | 1735 | next if ($type == 65281 || $type == 65284); | 
|---|
|  | 1736 | } | 
|---|
| [269] | 1737 |  | 
|---|
| [260] | 1738 | $host =~ s/ADMINDOMAIN/$config{domain}/g; | 
|---|
| [264] | 1739 |  | 
|---|
| [265] | 1740 | # Check to make sure the IP stubs will fit in the zone.  Under most usage failures here should be rare. | 
|---|
|  | 1741 | # On failure, tack a note on to a warning string and continue without adding this record. | 
|---|
|  | 1742 | # While we're at it, we substitute $zone for ZONE in the value. | 
|---|
|  | 1743 | if ($val eq 'ZONE') { | 
|---|
| [270] | 1744 | next if $revpatt;       # If we've got a pattern, we skip the default record version. | 
|---|
| [269] | 1745 | ##fixme?  do we care if we have multiple whole-zone templates? | 
|---|
| [265] | 1746 | $val = $zone->network; | 
|---|
|  | 1747 | } elsif ($val =~ /ZONE/) { | 
|---|
|  | 1748 | my $tmpval = $val; | 
|---|
|  | 1749 | $tmpval =~ s/ZONE//; | 
|---|
| [269] | 1750 | # Bend the rules and allow single-trailing-number PTR or PTR template records to be inserted | 
|---|
|  | 1751 | # as either v4 or v6.  May make this an off-by-default config flag | 
|---|
|  | 1752 | # Note that the origin records that may trigger this **SHOULD** already have ZONE,\d | 
|---|
|  | 1753 | if ($type == 12 || $type == 65282) { | 
|---|
|  | 1754 | $tmpval =~ s/[,.]/::/ if ($tmpval =~ /^[,.]\d+$/ && $zone->{isv6}); | 
|---|
|  | 1755 | $tmpval =~ s/[,:]+/./ if ($tmpval =~ /^(?:,|::)\d+$/ && !$zone->{isv6}); | 
|---|
|  | 1756 | } | 
|---|
| [265] | 1757 | my $addr; | 
|---|
|  | 1758 | if (_ipparent($dbh, 'n', 'y', \$tmpval, $rdns_id, \$addr)) { | 
|---|
|  | 1759 | $val = $addr->addr; | 
|---|
|  | 1760 | } else { | 
|---|
| [269] | 1761 | $warnstr .= "\nDefault record '$val $typemap{$type} $host' doesn't fit in $zone, skipping"; | 
|---|
| [264] | 1762 | next; | 
|---|
|  | 1763 | } | 
|---|
|  | 1764 | } | 
|---|
|  | 1765 |  | 
|---|
| [265] | 1766 | # Substitute $zone for ZONE in the hostname. | 
|---|
|  | 1767 | $host = _ZONE($zone, $host); | 
|---|
|  | 1768 |  | 
|---|
| [269] | 1769 | # Fill in the forward domain ID if we can find it, otherwise: | 
|---|
|  | 1770 | # Coerce type down to PTR or PTR template if we can't | 
|---|
|  | 1771 | my $domid = 0; | 
|---|
|  | 1772 | if ($type >= 65280) { | 
|---|
|  | 1773 | if (!($domid = _hostparent($dbh, $host))) { | 
|---|
|  | 1774 | $warnstr .= "\nRecord added as PTR instead of $typemap{$type};  domain not found for $host"; | 
|---|
|  | 1775 | $type = $reverse_typemap{PTR}; | 
|---|
|  | 1776 | $domid = 0;   # just to be explicit. | 
|---|
|  | 1777 | } | 
|---|
|  | 1778 | } | 
|---|
|  | 1779 |  | 
|---|
|  | 1780 | $sth_in->execute($domid,$host,$type,$val,$ttl); | 
|---|
|  | 1781 |  | 
|---|
| [260] | 1782 | if ($typemap{$type} eq 'SOA') { | 
|---|
|  | 1783 | my @tmp1 = split /:/, $host; | 
|---|
|  | 1784 | my @tmp2 = split /:/, $val; | 
|---|
| [286] | 1785 | _log($dbh, (rdns_id => $rdns_id, group_id => $group, | 
|---|
|  | 1786 | entry => "[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". | 
|---|
| [260] | 1787 | "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); | 
|---|
| [270] | 1788 | $defttl = $tmp2[3]; | 
|---|
| [260] | 1789 | } else { | 
|---|
|  | 1790 | my $logentry = "[new $zone] Added record '$host $typemap{$type}"; | 
|---|
| [286] | 1791 | _log($dbh, (rdns_id => $rdns_id, domain_id => $domid, group_id => $group, | 
|---|
|  | 1792 | entry => $logentry." $val', TTL $ttl")); | 
|---|
| [260] | 1793 | } | 
|---|
|  | 1794 | } | 
|---|
|  | 1795 |  | 
|---|
| [270] | 1796 | # Generate record based on provided pattern. | 
|---|
|  | 1797 | if ($revpatt) { | 
|---|
|  | 1798 | my $host; | 
|---|
|  | 1799 | my $type = ($zone->{isv6} ? 65284 : 65283); | 
|---|
|  | 1800 | my $val = $zone->network; | 
|---|
| [269] | 1801 |  | 
|---|
| [270] | 1802 | # Substitute $zone for ZONE in the hostname. | 
|---|
|  | 1803 | $host = _ZONE($zone, $revpatt); | 
|---|
|  | 1804 |  | 
|---|
|  | 1805 | my $domid = 0; | 
|---|
|  | 1806 | if (!($domid = _hostparent($dbh, $host))) { | 
|---|
|  | 1807 | $warnstr .= "\nDefault pattern added as PTR template instead of $typemap{$type};  domain not found for $host"; | 
|---|
|  | 1808 | $type = 65282; | 
|---|
|  | 1809 | $domid = 0;     # just to be explicit. | 
|---|
|  | 1810 | } | 
|---|
|  | 1811 |  | 
|---|
|  | 1812 | $sth_in->execute($domid,$host,$type,$val,$defttl); | 
|---|
| [286] | 1813 | my $logentry = "[new $zone] Added record '$host $typemap{$type}"; | 
|---|
|  | 1814 | _log($dbh, (rdns_id => $rdns_id, domain_id => $domid, group_id => $group, | 
|---|
|  | 1815 | entry => $logentry." $val', TTL $defttl from pattern")); | 
|---|
| [270] | 1816 | } | 
|---|
|  | 1817 |  | 
|---|
| [264] | 1818 | # If there are warnings (presumably about default records skipped for cause) log them | 
|---|
| [286] | 1819 | _log($dbh, (rdns_id => $rdns_id, group_id => $group, entry => "Warning(s) adding $zone:$warnstr")) | 
|---|
| [264] | 1820 | if $warnstr; | 
|---|
| [269] | 1821 |  | 
|---|
| [260] | 1822 | # once we get here, we should have suceeded. | 
|---|
|  | 1823 | $dbh->commit; | 
|---|
|  | 1824 | }; # end eval | 
|---|
|  | 1825 |  | 
|---|
|  | 1826 | if ($@) { | 
|---|
|  | 1827 | my $msg = $@; | 
|---|
|  | 1828 | eval { $dbh->rollback; }; | 
|---|
| [286] | 1829 | _log($dbh, (group_id => $group, entry => "Failed adding reverse zone $zone ($msg)")) | 
|---|
|  | 1830 | if $config{log_failures}; | 
|---|
|  | 1831 | $dbh->commit;       # since we enabled transactions earlier | 
|---|
| [260] | 1832 | return ('FAIL',$msg); | 
|---|
|  | 1833 | } else { | 
|---|
| [286] | 1834 | my $retcode = 'OK'; | 
|---|
|  | 1835 | if ($warnstr) { | 
|---|
|  | 1836 | $resultstr = $warnstr; | 
|---|
|  | 1837 | $retcode = 'WARN'; | 
|---|
|  | 1838 | } | 
|---|
|  | 1839 | return ($retcode, $rdns_id); | 
|---|
| [260] | 1840 | } | 
|---|
|  | 1841 |  | 
|---|
|  | 1842 | } # end addRDNS() | 
|---|
|  | 1843 |  | 
|---|
|  | 1844 |  | 
|---|
| [237] | 1845 | ## DNSDB::getZoneCount | 
|---|
|  | 1846 | # Get count of zones in group or groups | 
|---|
|  | 1847 | # Takes a database handle and hash containing: | 
|---|
|  | 1848 | #  - the "current" group | 
|---|
|  | 1849 | #  - an array of "acceptable" groups | 
|---|
|  | 1850 | #  - a flag for forward/reverse zones | 
|---|
|  | 1851 | #  - Optionally accept a "starts with" and/or "contains" filter argument | 
|---|
|  | 1852 | # Returns an integer count of the resulting zone list. | 
|---|
|  | 1853 | sub getZoneCount { | 
|---|
|  | 1854 | my $dbh = shift; | 
|---|
|  | 1855 |  | 
|---|
|  | 1856 | my %args = @_; | 
|---|
|  | 1857 |  | 
|---|
|  | 1858 | my @filterargs; | 
|---|
| [239] | 1859 | $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; | 
|---|
|  | 1860 | push @filterargs, "^$args{startwith}" if $args{startwith}; | 
|---|
|  | 1861 | $args{filter} =~ s/\./\[\.\]/g if $args{filter};      # only match literal dots, usually in reverse zones | 
|---|
| [237] | 1862 | push @filterargs, $args{filter} if $args{filter}; | 
|---|
|  | 1863 |  | 
|---|
|  | 1864 | my $sql; | 
|---|
|  | 1865 | # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read | 
|---|
|  | 1866 | if ($args{revrec} eq 'n') { | 
|---|
|  | 1867 | $sql = "SELECT count(*) FROM domains". | 
|---|
|  | 1868 | " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". | 
|---|
|  | 1869 | ($args{startwith} ? " AND domain ~* ?" : ''). | 
|---|
|  | 1870 | ($args{filter} ? " AND domain ~* ?" : ''); | 
|---|
|  | 1871 | } else { | 
|---|
|  | 1872 | $sql = "SELECT count(*) FROM revzones". | 
|---|
|  | 1873 | " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". | 
|---|
|  | 1874 | ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). | 
|---|
|  | 1875 | ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); | 
|---|
|  | 1876 | } | 
|---|
|  | 1877 | my ($count) = $dbh->selectrow_array($sql, undef, @filterargs); | 
|---|
|  | 1878 | return $count; | 
|---|
|  | 1879 | } # end getZoneCount() | 
|---|
|  | 1880 |  | 
|---|
|  | 1881 |  | 
|---|
|  | 1882 | ## DNSDB::getZoneList() | 
|---|
|  | 1883 | # Get a list of zones in the specified group(s) | 
|---|
|  | 1884 | # Takes the same arguments as getZoneCount() above | 
|---|
|  | 1885 | # Returns a reference to an array of hashrefs suitable for feeding to HTML::Template | 
|---|
|  | 1886 | sub getZoneList { | 
|---|
|  | 1887 | my $dbh = shift; | 
|---|
|  | 1888 |  | 
|---|
|  | 1889 | my %args = @_; | 
|---|
|  | 1890 |  | 
|---|
|  | 1891 | my @zonelist; | 
|---|
|  | 1892 |  | 
|---|
| [309] | 1893 | $args{sortorder} = 'ASC' if !grep /^$args{sortorder}$/, ('ASC','DESC'); | 
|---|
| [239] | 1894 | $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; | 
|---|
| [237] | 1895 |  | 
|---|
|  | 1896 | my @filterargs; | 
|---|
| [239] | 1897 | $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; | 
|---|
|  | 1898 | push @filterargs, "^$args{startwith}" if $args{startwith}; | 
|---|
|  | 1899 | $args{filter} =~ s/\./\[\.\]/g if $args{filter};      # only match literal dots, usually in reverse zones | 
|---|
| [237] | 1900 | push @filterargs, $args{filter} if $args{filter}; | 
|---|
|  | 1901 |  | 
|---|
|  | 1902 | my $sql; | 
|---|
|  | 1903 | # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read | 
|---|
|  | 1904 | if ($args{revrec} eq 'n') { | 
|---|
| [309] | 1905 | $args{sortby} = 'domain' if !grep /^$args{sortby}$/, ('domain','group','status'); | 
|---|
| [237] | 1906 | $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains". | 
|---|
|  | 1907 | " INNER JOIN groups ON domains.group_id=groups.group_id". | 
|---|
|  | 1908 | " WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". | 
|---|
|  | 1909 | ($args{startwith} ? " AND domain ~* ?" : ''). | 
|---|
|  | 1910 | ($args{filter} ? " AND domain ~* ?" : ''); | 
|---|
|  | 1911 | } else { | 
|---|
| [239] | 1912 | ##fixme:  arguably startwith here is irrelevant.  depends on the UI though. | 
|---|
| [309] | 1913 | $args{sortby} = 'revnet' if !grep /^$args{sortby}$/, ('revnet','group','status'); | 
|---|
| [237] | 1914 | $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones". | 
|---|
|  | 1915 | " INNER JOIN groups ON revzones.group_id=groups.group_id". | 
|---|
|  | 1916 | " WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". | 
|---|
|  | 1917 | ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). | 
|---|
|  | 1918 | ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); | 
|---|
|  | 1919 | } | 
|---|
|  | 1920 | # A common tail. | 
|---|
| [239] | 1921 | $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ". | 
|---|
|  | 1922 | ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}". | 
|---|
| [237] | 1923 | " OFFSET ".$args{offset}*$config{perpage}); | 
|---|
|  | 1924 | my $sth = $dbh->prepare($sql); | 
|---|
|  | 1925 | $sth->execute(@filterargs); | 
|---|
|  | 1926 | my $rownum = 0; | 
|---|
|  | 1927 |  | 
|---|
|  | 1928 | while (my @data = $sth->fetchrow_array) { | 
|---|
|  | 1929 | my %row; | 
|---|
|  | 1930 | $row{domainid} = $data[0]; | 
|---|
|  | 1931 | $row{domain} = $data[1]; | 
|---|
| [239] | 1932 | $row{status} = $data[2]; | 
|---|
| [237] | 1933 | $row{group} = $data[3]; | 
|---|
|  | 1934 | push @zonelist, \%row; | 
|---|
|  | 1935 | } | 
|---|
|  | 1936 |  | 
|---|
|  | 1937 | return \@zonelist; | 
|---|
|  | 1938 | } # end getZoneList() | 
|---|
|  | 1939 |  | 
|---|
|  | 1940 |  | 
|---|
| [18] | 1941 | ## DNSDB::addGroup() | 
|---|
|  | 1942 | # Add a group | 
|---|
| [66] | 1943 | # Takes a database handle, group name, parent group, hashref for permissions, | 
|---|
| [292] | 1944 | # and optional template-vs-cloneme flag for the default records | 
|---|
| [18] | 1945 | # Returns a status code and message | 
|---|
|  | 1946 | sub addGroup { | 
|---|
|  | 1947 | $errstr = ''; | 
|---|
|  | 1948 | my $dbh = shift; | 
|---|
| [20] | 1949 | my $groupname = shift; | 
|---|
|  | 1950 | my $pargroup = shift; | 
|---|
| [66] | 1951 | my $permissions = shift; | 
|---|
| [18] | 1952 |  | 
|---|
| [66] | 1953 | # 0 indicates "custom", hardcoded. | 
|---|
| [18] | 1954 | # Any other value clones that group's default records, if it exists. | 
|---|
| [66] | 1955 | my $inherit = shift || 0; | 
|---|
|  | 1956 | ##fixme:  need a flag to indicate clone records or <?> ? | 
|---|
| [18] | 1957 |  | 
|---|
|  | 1958 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 1959 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 1960 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 1961 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 1962 |  | 
|---|
| [292] | 1963 | my ($group_id) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname)); | 
|---|
| [38] | 1964 |  | 
|---|
|  | 1965 | return ('FAIL', "Group already exists") if $group_id; | 
|---|
|  | 1966 |  | 
|---|
| [18] | 1967 | # Wrap all the SQL in a transaction | 
|---|
|  | 1968 | eval { | 
|---|
| [292] | 1969 | $dbh->do("INSERT INTO groups (parent_group_id,group_name) VALUES (?,?)", undef, ($pargroup, $groupname) ); | 
|---|
| [18] | 1970 |  | 
|---|
| [292] | 1971 | my ($groupid) = $dbh->selectrow_array("SELECT currval('groups_group_id_seq')"); | 
|---|
| [18] | 1972 |  | 
|---|
| [292] | 1973 | # We work through the whole set of permissions instead of specifying them so | 
|---|
|  | 1974 | # that when we add a new permission, we don't have to change the code anywhere | 
|---|
|  | 1975 | # that doesn't explicitly deal with that specific permission. | 
|---|
|  | 1976 | my @permvals; | 
|---|
|  | 1977 | foreach (@permtypes) { | 
|---|
|  | 1978 | if (!defined ($permissions->{$_})) { | 
|---|
|  | 1979 | push @permvals, 0; | 
|---|
|  | 1980 | } else { | 
|---|
|  | 1981 | push @permvals, $permissions->{$_}; | 
|---|
| [66] | 1982 | } | 
|---|
| [292] | 1983 | } | 
|---|
|  | 1984 | $dbh->do("INSERT INTO permissions (group_id,$permlist) values (?".',?'x($#permtypes+1).")", | 
|---|
|  | 1985 | undef, ($groupid, @permvals) ); | 
|---|
|  | 1986 | my ($permid) = $dbh->selectrow_array("SELECT currval('permissions_permission_id_seq')"); | 
|---|
|  | 1987 | $dbh->do("UPDATE groups SET permission_id=$permid WHERE group_id=$groupid"); | 
|---|
| [66] | 1988 |  | 
|---|
| [292] | 1989 | # Default records | 
|---|
| [255] | 1990 | my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ". | 
|---|
| [20] | 1991 | "VALUES ($groupid,?,?,?,?,?,?,?)"); | 
|---|
| [255] | 1992 | my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ". | 
|---|
|  | 1993 | "VALUES ($groupid,?,?,?,?)"); | 
|---|
| [66] | 1994 | if ($inherit) { | 
|---|
| [87] | 1995 | # Duplicate records from parent.  Actually relying on inherited records feels | 
|---|
|  | 1996 | # very fragile, and it would be problematic to roll over at a later time. | 
|---|
| [18] | 1997 | my $sth2 = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?"); | 
|---|
| [87] | 1998 | $sth2->execute($pargroup); | 
|---|
| [18] | 1999 | while (my @clonedata = $sth2->fetchrow_array) { | 
|---|
| [255] | 2000 | $sthf->execute(@clonedata); | 
|---|
| [18] | 2001 | } | 
|---|
| [255] | 2002 | # And now the reverse records | 
|---|
| [292] | 2003 | $sth2 = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?"); | 
|---|
| [255] | 2004 | $sth2->execute($pargroup); | 
|---|
|  | 2005 | while (my @clonedata = $sth2->fetchrow_array) { | 
|---|
|  | 2006 | $sthr->execute(@clonedata); | 
|---|
|  | 2007 | } | 
|---|
| [18] | 2008 | } else { | 
|---|
| [66] | 2009 | ##fixme: Hardcoding is Bad, mmmmkaaaay? | 
|---|
| [18] | 2010 | # reasonable basic defaults for SOA, MX, NS, and minimal hosting | 
|---|
|  | 2011 | # could load from a config file, but somewhere along the line we need hardcoded bits. | 
|---|
| [255] | 2012 | $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400); | 
|---|
|  | 2013 | $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200); | 
|---|
|  | 2014 | $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200); | 
|---|
|  | 2015 | $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200); | 
|---|
|  | 2016 | $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200); | 
|---|
|  | 2017 | $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200); | 
|---|
|  | 2018 | # reasonable basic defaults for generic reverse zone.  Same as initial SQL tabledef. | 
|---|
|  | 2019 | $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400); | 
|---|
|  | 2020 | $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600); | 
|---|
| [18] | 2021 | } | 
|---|
|  | 2022 |  | 
|---|
| [292] | 2023 | _log($dbh, (group_id => $pargroup, entry => "Added group $groupname") ); | 
|---|
|  | 2024 |  | 
|---|
| [18] | 2025 | # once we get here, we should have suceeded. | 
|---|
|  | 2026 | $dbh->commit; | 
|---|
|  | 2027 | }; # end eval | 
|---|
|  | 2028 |  | 
|---|
|  | 2029 | if ($@) { | 
|---|
|  | 2030 | my $msg = $@; | 
|---|
|  | 2031 | eval { $dbh->rollback; }; | 
|---|
| [292] | 2032 | if ($config{log_failures}) { | 
|---|
|  | 2033 | _log($dbh, (group_id => $pargroup, entry => "Failed to add group $groupname: $msg") ); | 
|---|
|  | 2034 | $dbh->commit; | 
|---|
|  | 2035 | } | 
|---|
| [18] | 2036 | return ('FAIL',$msg); | 
|---|
|  | 2037 | } | 
|---|
|  | 2038 |  | 
|---|
| [292] | 2039 | return ('OK','OK'); | 
|---|
| [18] | 2040 | } # end addGroup() | 
|---|
|  | 2041 |  | 
|---|
|  | 2042 |  | 
|---|
| [22] | 2043 | ## DNSDB::delGroup() | 
|---|
|  | 2044 | # Delete a group. | 
|---|
|  | 2045 | # Takes a group ID | 
|---|
|  | 2046 | # Returns a status code and message | 
|---|
|  | 2047 | sub delGroup { | 
|---|
|  | 2048 | my $dbh = shift; | 
|---|
|  | 2049 | my $groupid = shift; | 
|---|
|  | 2050 |  | 
|---|
|  | 2051 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2052 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2053 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2054 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 2055 |  | 
|---|
|  | 2056 | ##fixme:  locate "knowable" error conditions and deal with them before the eval | 
|---|
| [23] | 2057 | # ... or inside, whatever. | 
|---|
| [22] | 2058 | # -> domains still exist in group | 
|---|
|  | 2059 | # -> ... | 
|---|
| [23] | 2060 | my $failmsg = ''; | 
|---|
| [293] | 2061 | my $resultmsg = ''; | 
|---|
| [22] | 2062 |  | 
|---|
| [293] | 2063 | # collect some pieces for logging and error messages | 
|---|
|  | 2064 | my $groupname = groupName($dbh,$groupid); | 
|---|
|  | 2065 | my $parid = parentID($dbh, (id => $groupid, type => 'group')); | 
|---|
|  | 2066 |  | 
|---|
| [22] | 2067 | # Wrap all the SQL in a transaction | 
|---|
|  | 2068 | eval { | 
|---|
| [293] | 2069 | # Check for Things in the group | 
|---|
|  | 2070 | $failmsg = "Can't remove group $groupname"; | 
|---|
|  | 2071 | my ($grpcnt) = $dbh->selectrow_array("SELECT count(*) FROM groups WHERE parent_group_id=?", undef, ($groupid)); | 
|---|
|  | 2072 | die "$grpcnt groups still in group\n" if $grpcnt; | 
|---|
|  | 2073 | my ($domcnt) = $dbh->selectrow_array("SELECT count(*) FROM domains WHERE group_id=?", undef, ($groupid)); | 
|---|
| [23] | 2074 | die "$domcnt domains still in group\n" if $domcnt; | 
|---|
| [293] | 2075 | my ($usercnt) = $dbh->selectrow_array("SELECT count(*) FROM users WHERE group_id=?", undef, ($groupid)); | 
|---|
|  | 2076 | die "$usercnt users still in group\n" if $usercnt; | 
|---|
| [23] | 2077 |  | 
|---|
| [293] | 2078 | $failmsg = "Failed to delete default records for $groupname"; | 
|---|
|  | 2079 | $dbh->do("DELETE from default_records WHERE group_id=?", undef, ($groupid)); | 
|---|
|  | 2080 | $failmsg = "Failed to delete default reverse records for $groupname"; | 
|---|
|  | 2081 | $dbh->do("DELETE from default_rev_records WHERE group_id=?", undef, ($groupid)); | 
|---|
|  | 2082 | $failmsg = "Failed to remove group $groupname"; | 
|---|
|  | 2083 | $dbh->do("DELETE from groups WHERE group_id=?", undef, ($groupid)); | 
|---|
| [22] | 2084 |  | 
|---|
| [293] | 2085 | _log($dbh, (group_id => $parid, entry => "Deleted group $groupname")); | 
|---|
|  | 2086 | $resultmsg = "Deleted group $groupname"; | 
|---|
|  | 2087 |  | 
|---|
| [22] | 2088 | # once we get here, we should have suceeded. | 
|---|
|  | 2089 | $dbh->commit; | 
|---|
|  | 2090 | }; # end eval | 
|---|
|  | 2091 |  | 
|---|
|  | 2092 | if ($@) { | 
|---|
|  | 2093 | my $msg = $@; | 
|---|
|  | 2094 | eval { $dbh->rollback; }; | 
|---|
| [293] | 2095 | if ($config{log_failures}) { | 
|---|
|  | 2096 | _log($dbh, (group_id => $parid, entry => "$failmsg: $msg")); | 
|---|
|  | 2097 | $dbh->commit;     # since we enabled transactions earlier | 
|---|
|  | 2098 | } | 
|---|
| [23] | 2099 | return ('FAIL',"$failmsg: $msg"); | 
|---|
| [22] | 2100 | } | 
|---|
| [293] | 2101 |  | 
|---|
|  | 2102 | return ('OK',$resultmsg); | 
|---|
| [22] | 2103 | } # end delGroup() | 
|---|
|  | 2104 |  | 
|---|
|  | 2105 |  | 
|---|
| [19] | 2106 | ## DNSDB::getChildren() | 
|---|
|  | 2107 | # Get a list of all groups whose parent^n is group <n> | 
|---|
| [24] | 2108 | # Takes a database handle, group ID, reference to an array to put the group IDs in, | 
|---|
|  | 2109 | # and an optional flag to return only immediate children or all children-of-children | 
|---|
|  | 2110 | # default to returning all children | 
|---|
| [19] | 2111 | # Calls itself | 
|---|
|  | 2112 | sub getChildren { | 
|---|
|  | 2113 | $errstr = ''; | 
|---|
|  | 2114 | my $dbh = shift; | 
|---|
| [20] | 2115 | my $rootgroup = shift; | 
|---|
|  | 2116 | my $groupdest = shift; | 
|---|
| [24] | 2117 | my $immed = shift || 'all'; | 
|---|
| [19] | 2118 |  | 
|---|
|  | 2119 | # special break for default group;  otherwise we get stuck. | 
|---|
| [20] | 2120 | if ($rootgroup == 1) { | 
|---|
| [19] | 2121 | # by definition, group 1 is the Root Of All Groups | 
|---|
| [24] | 2122 | my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE NOT (group_id=1)". | 
|---|
|  | 2123 | ($immed ne 'all' ? " AND parent_group_id=1" : '')); | 
|---|
| [19] | 2124 | $sth->execute; | 
|---|
|  | 2125 | while (my @this = $sth->fetchrow_array) { | 
|---|
| [20] | 2126 | push @$groupdest, @this; | 
|---|
| [19] | 2127 | } | 
|---|
|  | 2128 | } else { | 
|---|
|  | 2129 | my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE parent_group_id=?"); | 
|---|
| [20] | 2130 | $sth->execute($rootgroup); | 
|---|
| [19] | 2131 | return if $sth->rows == 0; | 
|---|
| [20] | 2132 | my @grouplist; | 
|---|
|  | 2133 | while (my ($group) = $sth->fetchrow_array) { | 
|---|
|  | 2134 | push @$groupdest, $group; | 
|---|
| [24] | 2135 | getChildren($dbh,$group,$groupdest) if $immed eq 'all'; | 
|---|
| [19] | 2136 | } | 
|---|
|  | 2137 | } | 
|---|
|  | 2138 | } # end getChildren() | 
|---|
|  | 2139 |  | 
|---|
|  | 2140 |  | 
|---|
| [20] | 2141 | ## DNSDB::groupName() | 
|---|
| [17] | 2142 | # Return the group name based on a group ID | 
|---|
|  | 2143 | # Takes a database handle and the group ID | 
|---|
|  | 2144 | # Returns the group name or undef on failure | 
|---|
| [20] | 2145 | sub groupName { | 
|---|
| [13] | 2146 | $errstr = ''; | 
|---|
|  | 2147 | my $dbh = shift; | 
|---|
| [20] | 2148 | my $groupid = shift; | 
|---|
|  | 2149 | my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?"); | 
|---|
|  | 2150 | $sth->execute($groupid); | 
|---|
|  | 2151 | my ($groupname) = $sth->fetchrow_array(); | 
|---|
|  | 2152 | $errstr = $DBI::errstr if !$groupname; | 
|---|
|  | 2153 | return $groupname if $groupname; | 
|---|
|  | 2154 | } # end groupName | 
|---|
| [13] | 2155 |  | 
|---|
|  | 2156 |  | 
|---|
| [314] | 2157 | ## DNSDB::getGroupCount() | 
|---|
|  | 2158 | # Get count of subgroups in group or groups | 
|---|
|  | 2159 | # Takes a database handle and hash containing: | 
|---|
|  | 2160 | #  - the "current" group | 
|---|
|  | 2161 | #  - an array of "acceptable" groups | 
|---|
|  | 2162 | #  - Optionally accept a "starts with" and/or "contains" filter argument | 
|---|
|  | 2163 | # Returns an integer count of the resulting group list. | 
|---|
|  | 2164 | sub getGroupCount { | 
|---|
|  | 2165 | my $dbh = shift; | 
|---|
|  | 2166 |  | 
|---|
|  | 2167 | my %args = @_; | 
|---|
|  | 2168 |  | 
|---|
|  | 2169 | my @filterargs; | 
|---|
|  | 2170 |  | 
|---|
|  | 2171 | $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; | 
|---|
|  | 2172 | push @filterargs, "^$args{startwith}" if $args{startwith}; | 
|---|
|  | 2173 | push @filterargs, $args{filter} if $args{filter}; | 
|---|
|  | 2174 |  | 
|---|
|  | 2175 | my $sql = "SELECT count(*) FROM groups ". | 
|---|
|  | 2176 | "WHERE parent_group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". | 
|---|
|  | 2177 | ($args{startwith} ? " AND group_name ~* ?" : ''). | 
|---|
|  | 2178 | ($args{filter} ? " AND group_name ~* ?" : ''); | 
|---|
|  | 2179 | my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) ); | 
|---|
|  | 2180 | $errstr = $dbh->errstr if !$count; | 
|---|
|  | 2181 | return $count; | 
|---|
|  | 2182 | } # end getGroupCount | 
|---|
|  | 2183 |  | 
|---|
|  | 2184 |  | 
|---|
|  | 2185 | ## DNSDB::getGroupList() | 
|---|
|  | 2186 | # Get a list of sub^n-groups in the specified group(s) | 
|---|
|  | 2187 | # Takes the same arguments as getGroupCount() above | 
|---|
|  | 2188 | # Returns an arrayref containing hashrefs suitable for feeding straight to HTML::Template | 
|---|
|  | 2189 | sub getGroupList { | 
|---|
|  | 2190 | my $dbh = shift; | 
|---|
|  | 2191 |  | 
|---|
|  | 2192 | my %args = @_; | 
|---|
|  | 2193 |  | 
|---|
|  | 2194 | my @filterargs; | 
|---|
|  | 2195 |  | 
|---|
|  | 2196 | $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; | 
|---|
|  | 2197 | push @filterargs, "^$args{startwith}" if $args{startwith}; | 
|---|
|  | 2198 | push @filterargs, $args{filter} if $args{filter}; | 
|---|
|  | 2199 |  | 
|---|
|  | 2200 | # protection against bad or missing arguments | 
|---|
|  | 2201 | $args{sortorder} = 'ASC' if !$args{sortorder}; | 
|---|
|  | 2202 | $args{offset} = 0 if !$args{offset}; | 
|---|
|  | 2203 |  | 
|---|
|  | 2204 | # munge sortby for columns in database | 
|---|
|  | 2205 | $args{sortby} = 'g.group_name' if $args{sortby} eq 'group'; | 
|---|
|  | 2206 | $args{sortby} = 'g2.group_name' if $args{sortby} eq 'parent'; | 
|---|
|  | 2207 |  | 
|---|
|  | 2208 | my $sql = q(SELECT g.group_id AS groupid, g.group_name AS groupname, g2.group_name AS pgroup, | 
|---|
|  | 2209 | count(distinct(u.username)) AS nusers, count(distinct(d.domain)) AS ndomains, | 
|---|
|  | 2210 | count(distinct(r.revnet)) AS nrevzones | 
|---|
|  | 2211 | FROM groups g | 
|---|
|  | 2212 | INNER JOIN groups g2 ON g2.group_id=g.parent_group_id | 
|---|
|  | 2213 | LEFT OUTER JOIN users u ON u.group_id=g.group_id | 
|---|
|  | 2214 | LEFT OUTER JOIN domains d ON d.group_id=g.group_id | 
|---|
|  | 2215 | LEFT OUTER JOIN revzones r ON r.group_id=g.group_id | 
|---|
|  | 2216 | ). | 
|---|
|  | 2217 | "WHERE g.parent_group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". | 
|---|
|  | 2218 | ($args{startwith} ? " AND g.group_name ~* ?" : ''). | 
|---|
|  | 2219 | ($args{filter} ? " AND g.group_name ~* ?" : ''). | 
|---|
|  | 2220 | " GROUP BY g.group_id, g.group_name, g2.group_name ". | 
|---|
|  | 2221 | " ORDER BY $args{sortby} $args{sortorder} ". | 
|---|
|  | 2222 | ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); | 
|---|
|  | 2223 | my $glist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) ); | 
|---|
|  | 2224 | $errstr = $dbh->errstr if !$glist; | 
|---|
|  | 2225 | return $glist; | 
|---|
|  | 2226 | } # end getGroupList | 
|---|
|  | 2227 |  | 
|---|
|  | 2228 |  | 
|---|
| [118] | 2229 | ## DNSDB::groupID() | 
|---|
|  | 2230 | # Return the group ID based on the group name | 
|---|
|  | 2231 | # Takes a database handle and the group name | 
|---|
|  | 2232 | # Returns the group ID or undef on failure | 
|---|
|  | 2233 | sub groupID { | 
|---|
|  | 2234 | $errstr = ''; | 
|---|
|  | 2235 | my $dbh = shift; | 
|---|
|  | 2236 | my $group = shift; | 
|---|
|  | 2237 | my ($grpid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group=?", undef, ($group) ); | 
|---|
|  | 2238 | $errstr = $DBI::errstr if !$grpid; | 
|---|
|  | 2239 | return $grpid if $grpid; | 
|---|
|  | 2240 | } # end groupID() | 
|---|
|  | 2241 |  | 
|---|
|  | 2242 |  | 
|---|
| [24] | 2243 | ## DNSDB::addUser() | 
|---|
| [87] | 2244 | # Add a user. | 
|---|
|  | 2245 | # Takes a DB handle, username, group ID, password, state (active/inactive). | 
|---|
|  | 2246 | # Optionally accepts: | 
|---|
|  | 2247 | #   user type (user/admin)      - defaults to user | 
|---|
|  | 2248 | #   permissions string          - defaults to inherit from group | 
|---|
|  | 2249 | #      three valid forms: | 
|---|
|  | 2250 | #       i                    - Inherit permissions | 
|---|
|  | 2251 | #       c:<user_id>          - Clone permissions from <user_id> | 
|---|
|  | 2252 | #       C:<permission list>  - Set these specific permissions | 
|---|
|  | 2253 | #   first name                  - defaults to username | 
|---|
|  | 2254 | #   last name                   - defaults to blank | 
|---|
|  | 2255 | #   phone                       - defaults to blank (could put other data within column def) | 
|---|
| [90] | 2256 | # Returns (OK,<uid>) on success, (FAIL,<message>) on failure | 
|---|
| [24] | 2257 | sub addUser { | 
|---|
|  | 2258 | $errstr = ''; | 
|---|
|  | 2259 | my $dbh = shift; | 
|---|
|  | 2260 | my $username = shift; | 
|---|
|  | 2261 | my $group = shift; | 
|---|
|  | 2262 | my $pass = shift; | 
|---|
|  | 2263 | my $state = shift; | 
|---|
| [25] | 2264 |  | 
|---|
| [90] | 2265 | return ('FAIL', "Missing one or more required entries") if !defined($state); | 
|---|
|  | 2266 | return ('FAIL', "Username must not be blank") if !$username; | 
|---|
| [87] | 2267 |  | 
|---|
| [25] | 2268 | my $type = shift || 'u';      # create limited users by default - fwiw, not sure yet how this will interact with ACLs | 
|---|
|  | 2269 |  | 
|---|
| [67] | 2270 | my $permstring = shift || 'i';        # default is to inhert permissions from group | 
|---|
|  | 2271 |  | 
|---|
| [25] | 2272 | my $fname = shift || $username; | 
|---|
| [24] | 2273 | my $lname = shift || ''; | 
|---|
| [25] | 2274 | my $phone = shift || '';      # not going format-check | 
|---|
| [24] | 2275 |  | 
|---|
| [38] | 2276 | my $sth = $dbh->prepare("SELECT user_id FROM users WHERE username=?"); | 
|---|
| [24] | 2277 | my $user_id; | 
|---|
|  | 2278 |  | 
|---|
| [38] | 2279 | # quick check to start to see if we've already got one | 
|---|
|  | 2280 | $sth->execute($username); | 
|---|
|  | 2281 | ($user_id) = $sth->fetchrow_array; | 
|---|
|  | 2282 |  | 
|---|
|  | 2283 | return ('FAIL', "User already exists") if $user_id; | 
|---|
|  | 2284 |  | 
|---|
| [24] | 2285 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2286 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2287 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2288 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 2289 |  | 
|---|
|  | 2290 | # Wrap all the SQL in a transaction | 
|---|
|  | 2291 | eval { | 
|---|
| [87] | 2292 | # insert the user...  note we set inherited perms by default since | 
|---|
|  | 2293 | # it's simple and cleans up some other bits of state | 
|---|
|  | 2294 | my $sth = $dbh->prepare("INSERT INTO users ". | 
|---|
|  | 2295 | "(group_id,username,password,firstname,lastname,phone,type,status,permission_id,inherit_perm) ". | 
|---|
|  | 2296 | "VALUES (?,?,?,?,?,?,?,?,(SELECT permission_id FROM permissions WHERE group_id=?),'t')"); | 
|---|
|  | 2297 | $sth->execute($group,$username,unix_md5_crypt($pass),$fname,$lname,$phone,$type,$state,$group); | 
|---|
| [24] | 2298 |  | 
|---|
|  | 2299 | # get the ID... | 
|---|
| [94] | 2300 | ($user_id) = $dbh->selectrow_array("SELECT currval('users_user_id_seq')"); | 
|---|
| [24] | 2301 |  | 
|---|
| [87] | 2302 | # Permissions!  Gotta set'em all! | 
|---|
|  | 2303 | die "Invalid permission string $permstring" | 
|---|
|  | 2304 | if $permstring !~ /^(?: | 
|---|
|  | 2305 | i       # inherit | 
|---|
|  | 2306 | |c:\d+  # clone | 
|---|
|  | 2307 | # custom.  no, the leading , is not a typo | 
|---|
| [111] | 2308 | |C:(?:,(?:group|user|domain|record|self)_(?:edit|create|delete))* | 
|---|
| [87] | 2309 | )$/x; | 
|---|
|  | 2310 | # bleh.  I'd call another function to do my dirty work, but we're in the middle of a transaction already. | 
|---|
|  | 2311 | if ($permstring ne 'i') { | 
|---|
|  | 2312 | # for cloned or custom permissions, we have to create a new permissions entry. | 
|---|
|  | 2313 | my $clonesrc = $group; | 
|---|
|  | 2314 | if ($permstring =~ /^c:(\d+)/) { $clonesrc = $1; } | 
|---|
|  | 2315 | $dbh->do("INSERT INTO permissions ($permlist,user_id) ". | 
|---|
|  | 2316 | "SELECT $permlist,? FROM permissions WHERE permission_id=". | 
|---|
|  | 2317 | "(SELECT permission_id FROM permissions WHERE ".($permstring =~ /^c:/ ? 'user' : 'group')."_id=?)", | 
|---|
|  | 2318 | undef, ($user_id,$clonesrc) ); | 
|---|
|  | 2319 | $dbh->do("UPDATE users SET permission_id=". | 
|---|
|  | 2320 | "(SELECT permission_id FROM permissions WHERE user_id=?) ". | 
|---|
|  | 2321 | "WHERE user_id=?", undef, ($user_id, $user_id) ); | 
|---|
|  | 2322 | } | 
|---|
|  | 2323 | if ($permstring =~ /^C:/) { | 
|---|
|  | 2324 | # finally for custom permissions, we set the passed-in permissions (and unset | 
|---|
|  | 2325 | # any that might have been brought in by the clone operation above) | 
|---|
|  | 2326 | my ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions WHERE user_id=?", | 
|---|
|  | 2327 | undef, ($user_id) ); | 
|---|
|  | 2328 | foreach (@permtypes) { | 
|---|
|  | 2329 | if ($permstring =~ /,$_/) { | 
|---|
|  | 2330 | $dbh->do("UPDATE permissions SET $_='t' WHERE permission_id=?", undef, ($permid) ); | 
|---|
|  | 2331 | } else { | 
|---|
|  | 2332 | $dbh->do("UPDATE permissions SET $_='f' WHERE permission_id=?", undef, ($permid) ); | 
|---|
|  | 2333 | } | 
|---|
|  | 2334 | } | 
|---|
|  | 2335 | } | 
|---|
|  | 2336 |  | 
|---|
|  | 2337 | $dbh->do("UPDATE users SET inherit_perm='n' WHERE user_id=?", undef, ($user_id) ); | 
|---|
|  | 2338 |  | 
|---|
| [25] | 2339 | ##fixme: add another table to hold name/email for log table? | 
|---|
|  | 2340 |  | 
|---|
| [294] | 2341 | _log($dbh, (group_id => $group, entry => "Added user $username ($fname $lname)")); | 
|---|
| [24] | 2342 | # once we get here, we should have suceeded. | 
|---|
|  | 2343 | $dbh->commit; | 
|---|
|  | 2344 | }; # end eval | 
|---|
|  | 2345 |  | 
|---|
|  | 2346 | if ($@) { | 
|---|
|  | 2347 | my $msg = $@; | 
|---|
|  | 2348 | eval { $dbh->rollback; }; | 
|---|
| [294] | 2349 | if ($config{log_failures}) { | 
|---|
|  | 2350 | _log($dbh, (group_id => $group, entry => "Error adding user $username: $msg")); | 
|---|
|  | 2351 | $dbh->commit;     # since we enabled transactions earlier | 
|---|
|  | 2352 | } | 
|---|
|  | 2353 | return ('FAIL',"Error adding user $username: $msg"); | 
|---|
| [24] | 2354 | } | 
|---|
| [294] | 2355 |  | 
|---|
|  | 2356 | return ('OK',"User $username ($fname $lname) added"); | 
|---|
| [24] | 2357 | } # end addUser | 
|---|
|  | 2358 |  | 
|---|
|  | 2359 |  | 
|---|
| [55] | 2360 | ## DNSDB::checkUser() | 
|---|
|  | 2361 | # Check user/pass combo on login | 
|---|
|  | 2362 | sub checkUser { | 
|---|
|  | 2363 | my $dbh = shift; | 
|---|
|  | 2364 | my $user = shift; | 
|---|
| [56] | 2365 | my $inpass = shift; | 
|---|
| [55] | 2366 |  | 
|---|
|  | 2367 | my $sth = $dbh->prepare("SELECT user_id,group_id,password,firstname,lastname FROM users WHERE username=?"); | 
|---|
|  | 2368 | $sth->execute($user); | 
|---|
|  | 2369 | my ($uid,$gid,$pass,$fname,$lname) = $sth->fetchrow_array; | 
|---|
|  | 2370 | my $loginfailed = 1 if !defined($uid); | 
|---|
|  | 2371 |  | 
|---|
|  | 2372 | if ($pass =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) { | 
|---|
| [56] | 2373 | $loginfailed = 1 if $pass ne unix_md5_crypt($inpass,$1); | 
|---|
| [55] | 2374 | } else { | 
|---|
| [56] | 2375 | $loginfailed = 1 if $pass ne $inpass; | 
|---|
| [55] | 2376 | } | 
|---|
|  | 2377 |  | 
|---|
|  | 2378 | # nnnngggg | 
|---|
|  | 2379 | return ($uid, $gid); | 
|---|
|  | 2380 | } # end checkUser | 
|---|
|  | 2381 |  | 
|---|
|  | 2382 |  | 
|---|
| [83] | 2383 | ## DNSDB:: updateUser() | 
|---|
| [90] | 2384 | # Update general data about user | 
|---|
| [83] | 2385 | sub updateUser { | 
|---|
|  | 2386 | my $dbh = shift; | 
|---|
| [118] | 2387 |  | 
|---|
|  | 2388 | ##fixme:  tweak calling convention so that we can update any given bit of data | 
|---|
| [83] | 2389 | my $uid = shift; | 
|---|
|  | 2390 | my $username = shift; | 
|---|
|  | 2391 | my $group = shift; | 
|---|
|  | 2392 | my $pass = shift; | 
|---|
|  | 2393 | my $state = shift; | 
|---|
| [87] | 2394 | my $type = shift || 'u'; | 
|---|
| [83] | 2395 | my $fname = shift || $username; | 
|---|
|  | 2396 | my $lname = shift || ''; | 
|---|
|  | 2397 | my $phone = shift || '';      # not going format-check | 
|---|
|  | 2398 |  | 
|---|
| [294] | 2399 | my $resultmsg = ''; | 
|---|
| [83] | 2400 |  | 
|---|
|  | 2401 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2402 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2403 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2404 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 2405 |  | 
|---|
|  | 2406 | my $sth; | 
|---|
|  | 2407 |  | 
|---|
|  | 2408 | # Password can be left blank;  if so we assume there's one on file. | 
|---|
|  | 2409 | # Actual blank passwords are bad, mm'kay? | 
|---|
|  | 2410 | if (!$pass) { | 
|---|
| [294] | 2411 | ($pass) = $dbh->selectrow_array("SELECT password FROM users WHERE user_id=?", undef, ($uid)); | 
|---|
| [83] | 2412 | } else { | 
|---|
|  | 2413 | $pass = unix_md5_crypt($pass); | 
|---|
|  | 2414 | } | 
|---|
|  | 2415 |  | 
|---|
|  | 2416 | eval { | 
|---|
| [294] | 2417 | $dbh->do("UPDATE users SET username=?, password=?, firstname=?, lastname=?, phone=?, type=?, status=?". | 
|---|
|  | 2418 | " WHERE user_id=?", undef, ($username, $pass, $fname, $lname, $phone, $type, $state, $uid)); | 
|---|
|  | 2419 | $resultmsg = "Updated user info for $username ($fname $lname)"; | 
|---|
|  | 2420 | _log($dbh, group_id => $group, entry => $resultmsg); | 
|---|
| [83] | 2421 | $dbh->commit; | 
|---|
|  | 2422 | }; | 
|---|
|  | 2423 | if ($@) { | 
|---|
|  | 2424 | my $msg = $@; | 
|---|
|  | 2425 | eval { $dbh->rollback; }; | 
|---|
| [294] | 2426 | if ($config{log_failures}) { | 
|---|
|  | 2427 | _log($dbh, (group_id => $group, entry => "Error updating user $username: $msg")); | 
|---|
|  | 2428 | $dbh->commit;     # since we enabled transactions earlier | 
|---|
|  | 2429 | } | 
|---|
|  | 2430 | return ('FAIL',"Error updating user $username: $msg"); | 
|---|
| [83] | 2431 | } | 
|---|
| [294] | 2432 |  | 
|---|
|  | 2433 | return ('OK',$resultmsg); | 
|---|
| [83] | 2434 | } # end updateUser() | 
|---|
|  | 2435 |  | 
|---|
|  | 2436 |  | 
|---|
| [24] | 2437 | ## DNSDB::delUser() | 
|---|
| [297] | 2438 | # Delete a user. | 
|---|
|  | 2439 | # Takes a database handle and user ID | 
|---|
|  | 2440 | # Returns a success/failure code and matching message | 
|---|
| [24] | 2441 | sub delUser { | 
|---|
| [25] | 2442 | my $dbh = shift; | 
|---|
|  | 2443 | my $userid = shift; | 
|---|
|  | 2444 |  | 
|---|
| [297] | 2445 | return ('FAIL',"Bad userid") if !defined($userid); | 
|---|
| [25] | 2446 |  | 
|---|
| [297] | 2447 | my $userdata = getUserData($dbh, $userid); | 
|---|
| [25] | 2448 |  | 
|---|
| [297] | 2449 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2450 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2451 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2452 | local $dbh->{RaiseError} = 1; | 
|---|
| [25] | 2453 |  | 
|---|
| [297] | 2454 | eval { | 
|---|
|  | 2455 | $dbh->do("DELETE FROM users WHERE user_id=?", undef, ($userid)); | 
|---|
|  | 2456 | _log($dbh, (group_id => $userdata->{group_id}, | 
|---|
|  | 2457 | entry => "Deleted user ID $userid/".$userdata->{username}. | 
|---|
|  | 2458 | " (".$userdata->{firstname}." ".$userdata->{lastname}.")") ); | 
|---|
|  | 2459 | $dbh->commit; | 
|---|
|  | 2460 | }; | 
|---|
|  | 2461 | if ($@) { | 
|---|
|  | 2462 | my $msg = $@; | 
|---|
|  | 2463 | eval { $dbh->rollback; }; | 
|---|
|  | 2464 | if ($config{log_failures}) { | 
|---|
|  | 2465 | _log($dbh, (group_id => $userdata->{group_id}, entry => "Error deleting user ID ". | 
|---|
|  | 2466 | "$userid/".$userdata->{username}.": $msg") ); | 
|---|
|  | 2467 | $dbh->commit; | 
|---|
|  | 2468 | } | 
|---|
|  | 2469 | return ('FAIL',"Error deleting user $userid/".$userdata->{username}.": $msg"); | 
|---|
|  | 2470 | } | 
|---|
|  | 2471 |  | 
|---|
|  | 2472 | return ('OK',"Deleted user ".$userdata->{username}." (".$userdata->{firstname}." ".$userdata->{lastname}.")"); | 
|---|
| [24] | 2473 | } # end delUser | 
|---|
|  | 2474 |  | 
|---|
|  | 2475 |  | 
|---|
| [25] | 2476 | ## DNSDB::userFullName() | 
|---|
|  | 2477 | # Return a pretty string! | 
|---|
|  | 2478 | # Takes a user_id and optional printf-ish string to indicate which pieces where: | 
|---|
|  | 2479 | # %u for the username | 
|---|
|  | 2480 | # %f for the first name | 
|---|
|  | 2481 | # %l for the last name | 
|---|
|  | 2482 | # All other text in the passed string will be left as-is. | 
|---|
|  | 2483 | ##fixme:  need a "smart" option too, so that missing/null/blank first/last names don't give funky output | 
|---|
|  | 2484 | sub userFullName { | 
|---|
|  | 2485 | $errstr = ''; | 
|---|
|  | 2486 | my $dbh = shift; | 
|---|
|  | 2487 | my $userid = shift; | 
|---|
|  | 2488 | my $fullformat = shift || '%f %l (%u)'; | 
|---|
|  | 2489 | my $sth = $dbh->prepare("select username,firstname,lastname from users where user_id=?"); | 
|---|
|  | 2490 | $sth->execute($userid); | 
|---|
|  | 2491 | my ($uname,$fname,$lname) = $sth->fetchrow_array(); | 
|---|
|  | 2492 | $errstr = $DBI::errstr if !$uname; | 
|---|
|  | 2493 |  | 
|---|
|  | 2494 | $fullformat =~ s/\%u/$uname/g; | 
|---|
|  | 2495 | $fullformat =~ s/\%f/$fname/g; | 
|---|
|  | 2496 | $fullformat =~ s/\%l/$lname/g; | 
|---|
|  | 2497 |  | 
|---|
|  | 2498 | return $fullformat; | 
|---|
|  | 2499 | } # end userFullName | 
|---|
|  | 2500 |  | 
|---|
|  | 2501 |  | 
|---|
| [51] | 2502 | ## DNSDB::userStatus() | 
|---|
|  | 2503 | # Sets and/or returns a user's status | 
|---|
|  | 2504 | # Takes a database handle, user ID and optionally a status argument | 
|---|
|  | 2505 | # Returns undef on errors. | 
|---|
|  | 2506 | sub userStatus { | 
|---|
|  | 2507 | my $dbh = shift; | 
|---|
|  | 2508 | my $id = shift; | 
|---|
| [296] | 2509 | my $newstatus = shift || 'mu'; | 
|---|
| [51] | 2510 |  | 
|---|
|  | 2511 | return undef if $id !~ /^\d+$/; | 
|---|
|  | 2512 |  | 
|---|
| [296] | 2513 | my $userdata = getUserData($dbh, $id); | 
|---|
| [51] | 2514 |  | 
|---|
| [296] | 2515 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2516 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2517 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2518 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 2519 |  | 
|---|
|  | 2520 | if ($newstatus ne 'mu') { | 
|---|
|  | 2521 | # ooo, fun!  let's see what we were passed for status | 
|---|
|  | 2522 | eval { | 
|---|
|  | 2523 | $newstatus = 0 if $newstatus eq 'useroff'; | 
|---|
|  | 2524 | $newstatus = 1 if $newstatus eq 'useron'; | 
|---|
|  | 2525 | $dbh->do("UPDATE users SET status=? WHERE user_id=?", undef, ($newstatus, $id)); | 
|---|
|  | 2526 |  | 
|---|
| [297] | 2527 | $resultstr = ($newstatus ? 'Enabled' : 'Disabled')." user ".$userdata->{username}. | 
|---|
|  | 2528 | " (".$userdata->{firstname}." ".$userdata->{lastname}.")"; | 
|---|
| [296] | 2529 |  | 
|---|
|  | 2530 | my %loghash; | 
|---|
|  | 2531 | $loghash{group_id} = parentID($dbh, (id => $id, type => 'user')); | 
|---|
|  | 2532 | $loghash{entry} = $resultstr; | 
|---|
|  | 2533 | _log($dbh, %loghash); | 
|---|
|  | 2534 |  | 
|---|
|  | 2535 | $dbh->commit; | 
|---|
|  | 2536 | }; | 
|---|
|  | 2537 | if ($@) { | 
|---|
|  | 2538 | my $msg = $@; | 
|---|
|  | 2539 | eval { $dbh->rollback; }; | 
|---|
|  | 2540 | $resultstr = ''; | 
|---|
|  | 2541 | $errstr = $msg; | 
|---|
|  | 2542 | ##fixme: failure logging? | 
|---|
|  | 2543 | return; | 
|---|
| [51] | 2544 | } | 
|---|
|  | 2545 | } | 
|---|
|  | 2546 |  | 
|---|
| [296] | 2547 | my ($status) = $dbh->selectrow_array("SELECT status FROM users WHERE user_id=?", undef, ($id)); | 
|---|
| [51] | 2548 | return $status; | 
|---|
|  | 2549 | } # end userStatus() | 
|---|
|  | 2550 |  | 
|---|
|  | 2551 |  | 
|---|
| [83] | 2552 | ## DNSDB::getUserData() | 
|---|
|  | 2553 | # Get misc user data for display | 
|---|
|  | 2554 | sub getUserData { | 
|---|
|  | 2555 | my $dbh = shift; | 
|---|
|  | 2556 | my $uid = shift; | 
|---|
|  | 2557 |  | 
|---|
|  | 2558 | my $sth = $dbh->prepare("SELECT group_id,username,firstname,lastname,phone,type,status,inherit_perm ". | 
|---|
|  | 2559 | "FROM users WHERE user_id=?"); | 
|---|
|  | 2560 | $sth->execute($uid); | 
|---|
|  | 2561 | return $sth->fetchrow_hashref(); | 
|---|
|  | 2562 |  | 
|---|
|  | 2563 | } # end getUserData() | 
|---|
|  | 2564 |  | 
|---|
|  | 2565 |  | 
|---|
| [2] | 2566 | ## DNSDB::getSOA() | 
|---|
|  | 2567 | # Return all suitable fields from an SOA record in separate elements of a hash | 
|---|
| [224] | 2568 | # Takes a database handle, default/live flag, domain/reverse flag, and parent ID | 
|---|
| [2] | 2569 | sub getSOA { | 
|---|
|  | 2570 | $errstr = ''; | 
|---|
|  | 2571 | my $dbh = shift; | 
|---|
|  | 2572 | my $def = shift; | 
|---|
| [224] | 2573 | my $rev = shift; | 
|---|
| [2] | 2574 | my $id = shift; | 
|---|
|  | 2575 |  | 
|---|
| [224] | 2576 | # (ab)use distance and weight columns to store SOA data?  can't for default_rev_records... | 
|---|
|  | 2577 | # - should really attach serial to the zone parent somewhere | 
|---|
| [101] | 2578 |  | 
|---|
| [224] | 2579 | my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev). | 
|---|
|  | 2580 | " WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}"; | 
|---|
| [311] | 2581 | my $ret = $dbh->selectrow_hashref($sql, undef, ($id) ); | 
|---|
|  | 2582 | return if !$ret; | 
|---|
| [246] | 2583 | ##fixme:  stick a flag somewhere if the record doesn't exist.  by the API, this is an impossible case, but... | 
|---|
| [2] | 2584 |  | 
|---|
| [311] | 2585 | ($ret->{contact},$ret->{prins}) = split /:/, $ret->{host}; | 
|---|
|  | 2586 | delete $ret->{host}; | 
|---|
|  | 2587 | ($ret->{refresh},$ret->{retry},$ret->{expire},$ret->{minttl}) = split /:/, $ret->{val}; | 
|---|
|  | 2588 | delete $ret->{val}; | 
|---|
| [2] | 2589 |  | 
|---|
| [311] | 2590 | return $ret; | 
|---|
| [2] | 2591 | } # end getSOA() | 
|---|
|  | 2592 |  | 
|---|
|  | 2593 |  | 
|---|
| [246] | 2594 | ## DNSDB::updateSOA() | 
|---|
|  | 2595 | # Update the specified SOA record | 
|---|
|  | 2596 | # Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash | 
|---|
| [277] | 2597 | # Returns a two-element list with a result code and message | 
|---|
| [246] | 2598 | sub updateSOA { | 
|---|
|  | 2599 | my $dbh = shift; | 
|---|
|  | 2600 | my $defrec = shift; | 
|---|
| [248] | 2601 | my $revrec = shift; | 
|---|
| [246] | 2602 |  | 
|---|
|  | 2603 | my %soa = @_; | 
|---|
|  | 2604 |  | 
|---|
| [311] | 2605 | my $oldsoa = getSOA($dbh, $defrec, $revrec, $soa{id}); | 
|---|
| [277] | 2606 |  | 
|---|
| [311] | 2607 | my $msg; | 
|---|
|  | 2608 | my %logdata; | 
|---|
|  | 2609 | if ($defrec eq 'n') { | 
|---|
|  | 2610 | $logdata{domain_id} = $soa{id} if $revrec eq 'n'; | 
|---|
|  | 2611 | $logdata{rdns_id} = $soa{id} if $revrec eq 'y'; | 
|---|
|  | 2612 | $logdata{group_id} = parentID($dbh, (id => $soa{id}, revrec => $revrec, | 
|---|
|  | 2613 | type => ($revrec eq 'n' ? 'domain' : 'revzone') ) ); | 
|---|
|  | 2614 | } else { | 
|---|
|  | 2615 | $logdata{group_id} = $soa{id}; | 
|---|
|  | 2616 | } | 
|---|
|  | 2617 | my $parname = ($defrec eq 'y' ? groupName($dbh, $soa{id}) : | 
|---|
|  | 2618 | ($revrec eq 'n' ? domainName($dbh, $soa{id}) : revName($dbh, $soa{id})) ); | 
|---|
|  | 2619 |  | 
|---|
| [277] | 2620 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2621 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2622 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2623 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 2624 |  | 
|---|
|  | 2625 | eval { | 
|---|
|  | 2626 | my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6"; | 
|---|
|  | 2627 | $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}", | 
|---|
| [311] | 2628 | $soa{ttl}, $oldsoa->{record_id}) ); | 
|---|
|  | 2629 | $msg = "Updated ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse ' : 'default ') : ''). | 
|---|
|  | 2630 | "SOA for $parname: ". | 
|---|
|  | 2631 | "(ns $oldsoa->{prins}, contact $oldsoa->{contact}, refresh $oldsoa->{refresh},". | 
|---|
|  | 2632 | " retry $oldsoa->{retry}, expire $oldsoa->{expire}, minTTL $oldsoa->{minttl}, TTL $oldsoa->{ttl}) to ". | 
|---|
| [277] | 2633 | "(ns $soa{prins}, contact $soa{contact}, refresh $soa{refresh},". | 
|---|
|  | 2634 | " retry $soa{retry}, expire $soa{expire}, minTTL $soa{minttl}, TTL $soa{ttl})"; | 
|---|
|  | 2635 |  | 
|---|
| [311] | 2636 | $logdata{entry} = $msg; | 
|---|
|  | 2637 | _log($dbh, %logdata); | 
|---|
| [277] | 2638 |  | 
|---|
|  | 2639 | $dbh->commit; | 
|---|
|  | 2640 | }; | 
|---|
|  | 2641 | if ($@) { | 
|---|
|  | 2642 | $msg = $@; | 
|---|
|  | 2643 | eval { $dbh->rollback; }; | 
|---|
| [311] | 2644 | $logdata{entry} = "Error updating ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse zone ' : 'default ') : ''). | 
|---|
|  | 2645 | "SOA record for $parname: $msg"; | 
|---|
|  | 2646 | if ($config{log_failures}) { | 
|---|
|  | 2647 | _log($dbh, %logdata); | 
|---|
|  | 2648 | $dbh->commit; | 
|---|
|  | 2649 | } | 
|---|
|  | 2650 | return ('FAIL', $logdata{entry}); | 
|---|
| [277] | 2651 | } else { | 
|---|
|  | 2652 | return ('OK', $msg); | 
|---|
|  | 2653 | } | 
|---|
| [246] | 2654 | } # end updateSOA() | 
|---|
|  | 2655 |  | 
|---|
|  | 2656 |  | 
|---|
| [2] | 2657 | ## DNSDB::getRecLine() | 
|---|
|  | 2658 | # Return all data fields for a zone record in separate elements of a hash | 
|---|
| [243] | 2659 | # Takes a database handle, default/live flag, forward/reverse flag, and record ID | 
|---|
| [2] | 2660 | sub getRecLine { | 
|---|
|  | 2661 | $errstr = ''; | 
|---|
|  | 2662 | my $dbh = shift; | 
|---|
| [243] | 2663 | my $defrec = shift; | 
|---|
|  | 2664 | my $revrec = shift; | 
|---|
| [2] | 2665 | my $id = shift; | 
|---|
|  | 2666 |  | 
|---|
| [243] | 2667 | my $sql = "SELECT record_id,host,type,val,ttl".($revrec eq 'n' ? ',distance,weight,port' : ''). | 
|---|
|  | 2668 | (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM '). | 
|---|
|  | 2669 | _rectable($defrec,$revrec)." WHERE record_id=?"; | 
|---|
| [123] | 2670 | my $ret = $dbh->selectrow_hashref($sql, undef, ($id) ); | 
|---|
| [2] | 2671 |  | 
|---|
| [90] | 2672 | if ($dbh->err) { | 
|---|
| [2] | 2673 | $errstr = $DBI::errstr; | 
|---|
|  | 2674 | return undef; | 
|---|
|  | 2675 | } | 
|---|
|  | 2676 |  | 
|---|
| [123] | 2677 | if (!$ret) { | 
|---|
|  | 2678 | $errstr = "No such record"; | 
|---|
|  | 2679 | return undef; | 
|---|
|  | 2680 | } | 
|---|
|  | 2681 |  | 
|---|
| [243] | 2682 | # explicitly set a parent id | 
|---|
|  | 2683 | if ($defrec eq 'y') { | 
|---|
|  | 2684 | $ret->{parid} = $ret->{group_id}; | 
|---|
|  | 2685 | } else { | 
|---|
|  | 2686 | $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id}); | 
|---|
|  | 2687 | # and a secondary if we have a custom type that lives in both a forward and reverse zone | 
|---|
|  | 2688 | $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279; | 
|---|
|  | 2689 | } | 
|---|
| [90] | 2690 |  | 
|---|
|  | 2691 | return $ret; | 
|---|
| [2] | 2692 | } | 
|---|
|  | 2693 |  | 
|---|
|  | 2694 |  | 
|---|
|  | 2695 | ##fixme: should use above (getRecLine()) to get lines for below? | 
|---|
|  | 2696 | ## DNSDB::getDomRecs() | 
|---|
|  | 2697 | # Return records for a domain | 
|---|
|  | 2698 | # Takes a database handle, default/live flag, group/domain ID, start, | 
|---|
|  | 2699 | # number of records, sort field, and sort order | 
|---|
|  | 2700 | # Returns a reference to an array of hashes | 
|---|
|  | 2701 | sub getDomRecs { | 
|---|
|  | 2702 | $errstr = ''; | 
|---|
|  | 2703 | my $dbh = shift; | 
|---|
| [224] | 2704 | my $def = shift; | 
|---|
|  | 2705 | my $rev = shift; | 
|---|
| [2] | 2706 | my $id = shift; | 
|---|
| [4] | 2707 | my $nrecs = shift || 'all'; | 
|---|
|  | 2708 | my $nstart = shift || 0; | 
|---|
| [2] | 2709 |  | 
|---|
| [4] | 2710 | ## for order, need to map input to column names | 
|---|
|  | 2711 | my $order = shift || 'host'; | 
|---|
| [72] | 2712 | my $direction = shift || 'ASC'; | 
|---|
| [4] | 2713 |  | 
|---|
| [135] | 2714 | my $filter = shift || ''; | 
|---|
|  | 2715 |  | 
|---|
| [224] | 2716 | my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl"; | 
|---|
|  | 2717 | $sql .= ",r.distance,r.weight,r.port" if $rev eq 'n'; | 
|---|
|  | 2718 | $sql .= " FROM "._rectable($def,$rev)." r "; | 
|---|
| [104] | 2719 | $sql .= "INNER JOIN rectypes t ON r.type=t.val ";     # for sorting by type alphabetically | 
|---|
| [224] | 2720 | $sql .= "WHERE "._recparent($def,$rev)." = ?"; | 
|---|
| [104] | 2721 | $sql .= " AND NOT r.type=$reverse_typemap{SOA}"; | 
|---|
| [160] | 2722 | $sql .= " AND host ~* ?" if $filter; | 
|---|
| [104] | 2723 | # use alphaorder column for "correct" ordering of sort-by-type instead of DNS RR type number | 
|---|
|  | 2724 | $sql .= " ORDER BY ".($order eq 'type' ? 't.alphaorder' : "r.$order")." $direction"; | 
|---|
| [4] | 2725 |  | 
|---|
| [222] | 2726 | my @bindvars = ($id); | 
|---|
|  | 2727 | push @bindvars, $filter if $filter; | 
|---|
| [224] | 2728 |  | 
|---|
|  | 2729 | # just to be ultraparanoid about SQL injection vectors | 
|---|
|  | 2730 | if ($nstart ne 'all') { | 
|---|
|  | 2731 | $sql .= " LIMIT ? OFFSET ?"; | 
|---|
|  | 2732 | push @bindvars, $nrecs; | 
|---|
|  | 2733 | push @bindvars, ($nstart*$nrecs); | 
|---|
|  | 2734 | } | 
|---|
| [90] | 2735 | my $sth = $dbh->prepare($sql) or warn $dbh->errstr; | 
|---|
| [222] | 2736 | $sth->execute(@bindvars) or warn "$sql: ".$sth->errstr; | 
|---|
| [2] | 2737 |  | 
|---|
|  | 2738 | my @retbase; | 
|---|
|  | 2739 | while (my $ref = $sth->fetchrow_hashref()) { | 
|---|
|  | 2740 | push @retbase, $ref; | 
|---|
|  | 2741 | } | 
|---|
|  | 2742 |  | 
|---|
|  | 2743 | my $ret = \@retbase; | 
|---|
|  | 2744 | return $ret; | 
|---|
|  | 2745 | } # end getDomRecs() | 
|---|
|  | 2746 |  | 
|---|
|  | 2747 |  | 
|---|
| [91] | 2748 | ## DNSDB::getRecCount() | 
|---|
| [224] | 2749 | # Return count of non-SOA records in zone (or default records in a group) | 
|---|
|  | 2750 | # Takes a database handle, default/live flag, reverse/forward flag, group/domain ID, | 
|---|
|  | 2751 | # and optional filtering modifier | 
|---|
| [91] | 2752 | # Returns the count | 
|---|
|  | 2753 | sub getRecCount { | 
|---|
|  | 2754 | my $dbh = shift; | 
|---|
|  | 2755 | my $defrec = shift; | 
|---|
| [224] | 2756 | my $revrec = shift; | 
|---|
| [91] | 2757 | my $id = shift; | 
|---|
| [135] | 2758 | my $filter = shift || ''; | 
|---|
| [91] | 2759 |  | 
|---|
| [135] | 2760 | # keep the nasties down, since we can't ?-sub this bit.  :/ | 
|---|
|  | 2761 | # note this is chars allowed in DNS hostnames | 
|---|
|  | 2762 | $filter =~ s/[^a-zA-Z0-9_.:-]//g; | 
|---|
|  | 2763 |  | 
|---|
| [222] | 2764 | my @bindvars = ($id); | 
|---|
|  | 2765 | push @bindvars, $filter if $filter; | 
|---|
| [224] | 2766 | my $sql = "SELECT count(*) FROM ". | 
|---|
|  | 2767 | _rectable($defrec,$revrec). | 
|---|
|  | 2768 | " WHERE "._recparent($defrec,$revrec)."=? ". | 
|---|
|  | 2769 | "AND NOT type=$reverse_typemap{SOA}". | 
|---|
|  | 2770 | ($filter ? " AND host ~* ?" : ''); | 
|---|
|  | 2771 | my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) ); | 
|---|
| [91] | 2772 |  | 
|---|
|  | 2773 | return $count; | 
|---|
|  | 2774 |  | 
|---|
|  | 2775 | } # end getRecCount() | 
|---|
|  | 2776 |  | 
|---|
|  | 2777 |  | 
|---|
| [3] | 2778 | ## DNSDB::addRec() | 
|---|
| [2] | 2779 | # Add a new record to a domain or a group's default records | 
|---|
|  | 2780 | # Takes a database handle, default/live flag, group/domain ID, | 
|---|
|  | 2781 | # host, type, value, and TTL | 
|---|
|  | 2782 | # Some types require additional detail: "distance" for MX and SRV, | 
|---|
|  | 2783 | # and weight/port for SRV | 
|---|
|  | 2784 | # Returns a status code and detail message in case of error | 
|---|
| [234] | 2785 | ##fixme:  pass a hash with the record data, not a series of separate values | 
|---|
| [2] | 2786 | sub addRec { | 
|---|
|  | 2787 | $errstr = ''; | 
|---|
|  | 2788 | my $dbh = shift; | 
|---|
|  | 2789 | my $defrec = shift; | 
|---|
| [226] | 2790 | my $revrec = shift; | 
|---|
|  | 2791 | my $id = shift;       # parent (group_id for defrecs, rdns_id for reverse records, | 
|---|
|  | 2792 | # domain_id for domain records) | 
|---|
| [2] | 2793 |  | 
|---|
|  | 2794 | my $host = shift; | 
|---|
| [234] | 2795 | my $rectype = shift;  # reference so we can coerce it if "+"-types can't find both zones | 
|---|
| [2] | 2796 | my $val = shift; | 
|---|
|  | 2797 | my $ttl = shift; | 
|---|
|  | 2798 |  | 
|---|
| [226] | 2799 | # prep for validation | 
|---|
| [252] | 2800 | my $addr = NetAddr::IP->new($$val); | 
|---|
|  | 2801 | $$host =~ s/\.+$//;   # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI. | 
|---|
| [226] | 2802 |  | 
|---|
|  | 2803 | my $domid = 0; | 
|---|
|  | 2804 | my $revid = 0; | 
|---|
|  | 2805 |  | 
|---|
|  | 2806 | my $retcode = 'OK';   # assume everything will go OK | 
|---|
|  | 2807 | my $retmsg = ''; | 
|---|
|  | 2808 |  | 
|---|
|  | 2809 | # do simple validation first | 
|---|
|  | 2810 | return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/; | 
|---|
|  | 2811 |  | 
|---|
| [234] | 2812 | # Quick check on hostname parts.  Note the regex is more forgiving than the error message; | 
|---|
|  | 2813 | # domain names technically are case-insensitive, and we use printf-like % codes for a couple | 
|---|
|  | 2814 | # of types.  Other things may also be added to validate default records of several flavours. | 
|---|
|  | 2815 | return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)") | 
|---|
| [272] | 2816 | if $defrec eq 'n' && $$host !~ /^[0-9a-z_%.-]+$/i; | 
|---|
| [226] | 2817 |  | 
|---|
| [234] | 2818 | # Collect these even if we're only doing a simple A record so we can call *any* validation sub | 
|---|
|  | 2819 | my $dist = shift; | 
|---|
| [281] | 2820 | my $weight = shift; | 
|---|
| [234] | 2821 | my $port = shift; | 
|---|
| [226] | 2822 |  | 
|---|
| [234] | 2823 | my $fields; | 
|---|
|  | 2824 | my @vallist; | 
|---|
| [226] | 2825 |  | 
|---|
| [234] | 2826 | # Call the validation sub for the type requested. | 
|---|
|  | 2827 | ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id, | 
|---|
| [249] | 2828 | host => $host, rectype => $rectype, val => $val, addr => $addr, | 
|---|
| [234] | 2829 | dist => \$dist, port => \$port, weight => \$weight, | 
|---|
|  | 2830 | fields => \$fields, vallist => \@vallist) ); | 
|---|
| [129] | 2831 |  | 
|---|
| [234] | 2832 | return ($retcode,$retmsg) if $retcode eq 'FAIL'; | 
|---|
| [209] | 2833 |  | 
|---|
| [234] | 2834 | # Set up database fields and bind parameters | 
|---|
|  | 2835 | $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec); | 
|---|
| [249] | 2836 | push @vallist, ($$host,$$rectype,$$val,$ttl,$id); | 
|---|
| [234] | 2837 | my $vallen = '?'.(',?'x$#vallist); | 
|---|
| [2] | 2838 |  | 
|---|
| [287] | 2839 | # Put together the success log entry.  We have to use this horrible kludge | 
|---|
|  | 2840 | # because domain_id and rdns_id may or may not be present, and if they are, | 
|---|
|  | 2841 | # they're not at a guaranteed consistent index in the array.  wheee! | 
|---|
|  | 2842 | my %logdata; | 
|---|
|  | 2843 | my @ftmp = split /,/, $fields; | 
|---|
|  | 2844 | for (my $i=0; $i <= $#vallist; $i++) { | 
|---|
|  | 2845 | $logdata{domain_id} = $vallist[$i] if $ftmp[$i] eq 'domain_id'; | 
|---|
|  | 2846 | $logdata{rdns_id} = $vallist[$i] if $ftmp[$i] eq 'rdns_id'; | 
|---|
|  | 2847 | } | 
|---|
|  | 2848 | $logdata{group_id} = $id if $defrec eq 'y'; | 
|---|
|  | 2849 | $logdata{group_id} = parentID($dbh, | 
|---|
|  | 2850 | (id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ) | 
|---|
|  | 2851 | if $defrec eq 'n'; | 
|---|
| [288] | 2852 | $logdata{entry} = "Added ".($defrec eq 'y' ? 'default record' : 'record')." '$$host $typemap{$$rectype} $$val"; | 
|---|
| [287] | 2853 | $logdata{entry} .= " [distance $dist]" if $typemap{$$rectype} eq 'MX'; | 
|---|
|  | 2854 | $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]" | 
|---|
|  | 2855 | if $typemap{$$rectype} eq 'SRV'; | 
|---|
| [288] | 2856 | $logdata{entry} .= "', TTL $ttl"; | 
|---|
| [287] | 2857 |  | 
|---|
| [90] | 2858 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 2859 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 2860 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2861 | local $dbh->{RaiseError} = 1; | 
|---|
| [2] | 2862 |  | 
|---|
| [90] | 2863 | eval { | 
|---|
| [236] | 2864 | $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)", | 
|---|
| [90] | 2865 | undef, @vallist); | 
|---|
| [287] | 2866 | _log($dbh, %logdata); | 
|---|
| [90] | 2867 | $dbh->commit; | 
|---|
|  | 2868 | }; | 
|---|
|  | 2869 | if ($@) { | 
|---|
|  | 2870 | my $msg = $@; | 
|---|
|  | 2871 | eval { $dbh->rollback; }; | 
|---|
| [289] | 2872 | if ($config{log_failures}) { | 
|---|
|  | 2873 | $logdata{entry} = "Failed adding ".($defrec eq 'y' ? 'default ' : ''). | 
|---|
|  | 2874 | "record '$$host $typemap{$$rectype} $$val', TTL $ttl ($msg)"; | 
|---|
|  | 2875 | _log($dbh, %logdata); | 
|---|
|  | 2876 | $dbh->commit; | 
|---|
|  | 2877 | } | 
|---|
| [90] | 2878 | return ('FAIL',$msg); | 
|---|
|  | 2879 | } | 
|---|
| [2] | 2880 |  | 
|---|
| [287] | 2881 | $resultstr = $logdata{entry}; | 
|---|
| [226] | 2882 | return ($retcode, $retmsg); | 
|---|
| [90] | 2883 |  | 
|---|
| [2] | 2884 | } # end addRec() | 
|---|
|  | 2885 |  | 
|---|
|  | 2886 |  | 
|---|
| [16] | 2887 | ## DNSDB::updateRec() | 
|---|
|  | 2888 | # Update a record | 
|---|
| [273] | 2889 | # Takes a database handle, default and reverse flags, record ID, immediate parent ID, and new record data. | 
|---|
|  | 2890 | # Returns a status code and message | 
|---|
| [16] | 2891 | sub updateRec { | 
|---|
|  | 2892 | $errstr = ''; | 
|---|
| [17] | 2893 |  | 
|---|
| [16] | 2894 | my $dbh = shift; | 
|---|
|  | 2895 | my $defrec = shift; | 
|---|
| [272] | 2896 | my $revrec = shift; | 
|---|
| [16] | 2897 | my $id = shift; | 
|---|
| [272] | 2898 | my $parid = shift;    # immediate parent entity that we're descending from to update the record | 
|---|
| [16] | 2899 |  | 
|---|
| [273] | 2900 | # all records have these | 
|---|
| [16] | 2901 | my $host = shift; | 
|---|
| [272] | 2902 | my $hostbk = $$host;  # Keep a backup copy of the original, so we can WARN if the update mangles the domain | 
|---|
|  | 2903 | my $rectype = shift; | 
|---|
| [16] | 2904 | my $val = shift; | 
|---|
|  | 2905 | my $ttl = shift; | 
|---|
|  | 2906 |  | 
|---|
| [272] | 2907 | # prep for validation | 
|---|
|  | 2908 | my $addr = NetAddr::IP->new($$val); | 
|---|
|  | 2909 | $$host =~ s/\.+$//;   # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI. | 
|---|
| [16] | 2910 |  | 
|---|
| [272] | 2911 | my $domid = 0; | 
|---|
|  | 2912 | my $revid = 0; | 
|---|
|  | 2913 |  | 
|---|
|  | 2914 | my $retcode = 'OK';   # assume everything will go OK | 
|---|
|  | 2915 | my $retmsg = ''; | 
|---|
|  | 2916 |  | 
|---|
| [273] | 2917 | # do simple validation first | 
|---|
| [272] | 2918 | return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/; | 
|---|
|  | 2919 |  | 
|---|
|  | 2920 | # Quick check on hostname parts.  Note the regex is more forgiving than the error message; | 
|---|
|  | 2921 | # domain names technically are case-insensitive, and we use printf-like % codes for a couple | 
|---|
|  | 2922 | # of types.  Other things may also be added to validate default records of several flavours. | 
|---|
| [273] | 2923 | return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z - . _)") | 
|---|
| [272] | 2924 | if $defrec eq 'n' && $$host !~ /^[0-9a-z_%.-]+$/i; | 
|---|
|  | 2925 |  | 
|---|
| [273] | 2926 | # only MX and SRV will use these | 
|---|
| [288] | 2927 | my $dist = shift || 0; | 
|---|
|  | 2928 | my $weight = shift || 0; | 
|---|
|  | 2929 | my $port = shift || 0; | 
|---|
| [16] | 2930 |  | 
|---|
| [272] | 2931 | my $fields; | 
|---|
|  | 2932 | my @vallist; | 
|---|
| [16] | 2933 |  | 
|---|
| [273] | 2934 | # get old record data so we have the right parent ID | 
|---|
|  | 2935 | # and for logging (eventually) | 
|---|
|  | 2936 | my $oldrec = getRecLine($dbh, $defrec, $revrec, $id); | 
|---|
| [223] | 2937 |  | 
|---|
| [272] | 2938 | # Call the validation sub for the type requested. | 
|---|
|  | 2939 | # Note the ID to pass here is the *parent*, not the record | 
|---|
|  | 2940 | ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, | 
|---|
|  | 2941 | id => ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})), | 
|---|
|  | 2942 | host => $host, rectype => $rectype, val => $val, addr => $addr, | 
|---|
|  | 2943 | dist => \$dist, port => \$port, weight => \$weight, | 
|---|
|  | 2944 | fields => \$fields, vallist => \@vallist, | 
|---|
|  | 2945 | update => $id) ); | 
|---|
|  | 2946 |  | 
|---|
|  | 2947 | return ($retcode,$retmsg) if $retcode eq 'FAIL'; | 
|---|
|  | 2948 |  | 
|---|
| [273] | 2949 | # Set up database fields and bind parameters.  Note only the optional fields | 
|---|
|  | 2950 | # (distance, weight, port, secondary parent ID) are added in the validation call above | 
|---|
|  | 2951 | $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec); | 
|---|
|  | 2952 | push @vallist, ($$host,$$rectype,$$val,$ttl, | 
|---|
|  | 2953 | ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})) ); | 
|---|
| [272] | 2954 |  | 
|---|
| [273] | 2955 | # hack hack PTHUI | 
|---|
|  | 2956 | # need to forcibly make sure we disassociate a record with a parent it's no longer related to. | 
|---|
|  | 2957 | # eg, PTR records may not have a domain parent, or A/AAAA records may not have a revzone parent. | 
|---|
|  | 2958 | # mainly needed for crossover types that got coerced down to "standard" types | 
|---|
|  | 2959 | if ($defrec eq 'n') { | 
|---|
|  | 2960 | if ($$rectype == $reverse_typemap{PTR}) { | 
|---|
|  | 2961 | $fields .= ",domain_id"; | 
|---|
|  | 2962 | push @vallist, 0; | 
|---|
|  | 2963 | } | 
|---|
|  | 2964 | if ($$rectype == $reverse_typemap{A} || $$rectype == $reverse_typemap{AAAA}) { | 
|---|
|  | 2965 | $fields .= ",rdns_id"; | 
|---|
|  | 2966 | push @vallist, 0; | 
|---|
|  | 2967 | } | 
|---|
|  | 2968 | } | 
|---|
| [272] | 2969 |  | 
|---|
| [273] | 2970 | # Fiddle the field list into something suitable for updates | 
|---|
|  | 2971 | $fields =~ s/,/=?,/g; | 
|---|
|  | 2972 | $fields .= "=?"; | 
|---|
| [272] | 2973 |  | 
|---|
| [288] | 2974 | # Put together the success log entry.  Horrible kludge from addRec() copied as-is since | 
|---|
|  | 2975 | # we don't know whether the passed arguments or retrieved values for domain_id and rdns_id | 
|---|
|  | 2976 | # will be maintained (due to "not-in-zone" validation changes) | 
|---|
|  | 2977 | my %logdata; | 
|---|
|  | 2978 | my @ftmp = split /,/, $fields; | 
|---|
|  | 2979 | for (my $i=0; $i <= $#vallist; $i++) { | 
|---|
|  | 2980 | $logdata{domain_id} = $vallist[$i] if $ftmp[$i] eq 'domain_id'; | 
|---|
|  | 2981 | $logdata{rdns_id} = $vallist[$i] if $ftmp[$i] eq 'rdns_id'; | 
|---|
|  | 2982 | } | 
|---|
|  | 2983 | $logdata{group_id} = $parid if $defrec eq 'y'; | 
|---|
|  | 2984 | $logdata{group_id} = parentID($dbh, | 
|---|
|  | 2985 | (id => $parid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ) | 
|---|
|  | 2986 | if $defrec eq 'n'; | 
|---|
|  | 2987 | $logdata{entry} = "Updated ".($defrec eq 'y' ? 'default record' : 'record')." from\n". | 
|---|
|  | 2988 | "'$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}"; | 
|---|
|  | 2989 | $logdata{entry} .= " [distance $oldrec->{distance}]" if $typemap{$oldrec->{type}} eq 'MX'; | 
|---|
|  | 2990 | $logdata{entry} .= " [priority $oldrec->{distance}] [weight $oldrec->{weight}] [port $oldrec->{port}]" | 
|---|
|  | 2991 | if $typemap{$oldrec->{type}} eq 'SRV'; | 
|---|
|  | 2992 | $logdata{entry} .= "', TTL $oldrec->{ttl}\nto\n'$$host $typemap{$$rectype} $$val"; | 
|---|
|  | 2993 | $logdata{entry} .= " [distance $dist]" if $typemap{$$rectype} eq 'MX'; | 
|---|
|  | 2994 | $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$$rectype} eq 'SRV'; | 
|---|
|  | 2995 | $logdata{entry} .= "', TTL $ttl"; | 
|---|
|  | 2996 |  | 
|---|
| [90] | 2997 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 2998 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 2999 |  | 
|---|
|  | 3000 | eval { | 
|---|
| [273] | 3001 | $dbh->do("UPDATE "._rectable($defrec,$revrec)." SET $fields WHERE record_id=?", undef, (@vallist, $id) ); | 
|---|
| [288] | 3002 | _log($dbh, %logdata); | 
|---|
| [130] | 3003 | $dbh->commit; | 
|---|
| [90] | 3004 | }; | 
|---|
|  | 3005 | if ($@) { | 
|---|
|  | 3006 | my $msg = $@; | 
|---|
| [288] | 3007 | eval { $dbh->rollback; }; | 
|---|
| [289] | 3008 | if ($config{log_failures}) { | 
|---|
|  | 3009 | $logdata{entry} = "Failed updating ".($defrec eq 'y' ? 'default ' : ''). | 
|---|
|  | 3010 | "record '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)"; | 
|---|
|  | 3011 | _log($dbh, %logdata); | 
|---|
|  | 3012 | $dbh->commit; | 
|---|
|  | 3013 | } | 
|---|
| [90] | 3014 | return ('FAIL', $msg); | 
|---|
|  | 3015 | } | 
|---|
|  | 3016 |  | 
|---|
| [288] | 3017 | $resultstr = $logdata{entry}; | 
|---|
| [272] | 3018 | return ($retcode, $retmsg); | 
|---|
| [16] | 3019 | } # end updateRec() | 
|---|
|  | 3020 |  | 
|---|
|  | 3021 |  | 
|---|
| [3] | 3022 | ## DNSDB::delRec() | 
|---|
|  | 3023 | # Delete a record. | 
|---|
|  | 3024 | sub delRec { | 
|---|
|  | 3025 | $errstr = ''; | 
|---|
|  | 3026 | my $dbh = shift; | 
|---|
|  | 3027 | my $defrec = shift; | 
|---|
| [243] | 3028 | my $revrec = shift; | 
|---|
| [3] | 3029 | my $id = shift; | 
|---|
|  | 3030 |  | 
|---|
| [290] | 3031 | my $oldrec = getRecLine($dbh, $defrec, $revrec, $id); | 
|---|
| [3] | 3032 |  | 
|---|
| [290] | 3033 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 3034 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 3035 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 3036 | local $dbh->{RaiseError} = 1; | 
|---|
| [3] | 3037 |  | 
|---|
| [290] | 3038 | # Put together the log entry | 
|---|
|  | 3039 | my %logdata; | 
|---|
|  | 3040 | $logdata{domain_id} = $oldrec->{domain_id}; | 
|---|
|  | 3041 | $logdata{rdns_id} = $oldrec->{rdns_id}; | 
|---|
|  | 3042 | $logdata{group_id} = $oldrec->{group_id} if $defrec eq 'y'; | 
|---|
|  | 3043 | $logdata{group_id} = parentID($dbh, | 
|---|
|  | 3044 | (id => $oldrec->{domain_id}, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ) | 
|---|
|  | 3045 | if $defrec eq 'n'; | 
|---|
|  | 3046 | $logdata{entry} = "Deleted ".($defrec eq 'y' ? 'default record ' : 'record '). | 
|---|
|  | 3047 | "'$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}"; | 
|---|
|  | 3048 | $logdata{entry} .= " [distance $oldrec->{distance}]" if $typemap{$oldrec->{type}} eq 'MX'; | 
|---|
|  | 3049 | $logdata{entry} .= " [priority $oldrec->{distance}] [weight $oldrec->{weight}] [port $oldrec->{port}]" | 
|---|
|  | 3050 | if $typemap{$oldrec->{type}} eq 'SRV'; | 
|---|
|  | 3051 | $logdata{entry} .= "', TTL $oldrec->{ttl}\n"; | 
|---|
|  | 3052 |  | 
|---|
|  | 3053 | eval { | 
|---|
|  | 3054 | my $sth = $dbh->do("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?", undef, ($id)); | 
|---|
|  | 3055 | _log($dbh, %logdata); | 
|---|
|  | 3056 | $dbh->commit; | 
|---|
|  | 3057 | }; | 
|---|
|  | 3058 | if ($@) { | 
|---|
|  | 3059 | my $msg = $@; | 
|---|
|  | 3060 | eval { $dbh->rollback; }; | 
|---|
|  | 3061 | if ($config{log_failures}) { | 
|---|
|  | 3062 | $logdata{entry} = "Error deleting ".($defrec eq 'y' ? 'default record' : 'record'). | 
|---|
|  | 3063 | " '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)"; | 
|---|
|  | 3064 | _log($dbh, %logdata); | 
|---|
|  | 3065 | $dbh->commit; | 
|---|
|  | 3066 | } | 
|---|
|  | 3067 | return ('FAIL', $msg); | 
|---|
|  | 3068 | } | 
|---|
|  | 3069 |  | 
|---|
|  | 3070 | return ('OK',$logdata{entry}); | 
|---|
| [3] | 3071 | } # end delRec() | 
|---|
|  | 3072 |  | 
|---|
|  | 3073 |  | 
|---|
| [323] | 3074 | ## DNSDB::getLogCount() | 
|---|
|  | 3075 | # Get a count of log entries | 
|---|
|  | 3076 | # Takes a database handle and a hash containing at least: | 
|---|
|  | 3077 | # - Entity ID and entity type as the primary log "slice" | 
|---|
|  | 3078 | sub getLogCount { | 
|---|
|  | 3079 | my $dbh = shift; | 
|---|
|  | 3080 |  | 
|---|
|  | 3081 | my %args = @_; | 
|---|
|  | 3082 |  | 
|---|
|  | 3083 | my @filterargs; | 
|---|
|  | 3084 | ##fixme:  which fields do we want to filter on? | 
|---|
|  | 3085 | # push @filterargs, | 
|---|
|  | 3086 |  | 
|---|
|  | 3087 | $errstr = 'Missing primary parent ID and/or type'; | 
|---|
|  | 3088 | # fail early if we don't have a "prime" ID to look for log entries for | 
|---|
|  | 3089 | return if !$args{id}; | 
|---|
|  | 3090 |  | 
|---|
|  | 3091 | # or if the prime id type is missing or invalid | 
|---|
|  | 3092 | return if !$args{logtype}; | 
|---|
|  | 3093 | $args{logtype} = 'revzone' if $args{logtype} eq 'rdns';       # hack pthui | 
|---|
|  | 3094 | $args{logtype} = 'domain' if $args{logtype} eq 'dom';         # hack pthui | 
|---|
|  | 3095 | return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user'); | 
|---|
|  | 3096 |  | 
|---|
|  | 3097 | $args{logtype} = 'revzone' if $args{logtype} eq 'rdns';       # hack pthui | 
|---|
|  | 3098 |  | 
|---|
|  | 3099 | my $sql = "SELECT count(*) FROM log ". | 
|---|
|  | 3100 | "WHERE $id_col{$args{logtype}}=?". | 
|---|
|  | 3101 | ($args{filter} ? " AND entry ~* ?" : ''); | 
|---|
|  | 3102 | my ($count) = $dbh->selectrow_array($sql, undef, ($args{id}, @filterargs) ); | 
|---|
|  | 3103 | $errstr = $dbh->errstr if !$count; | 
|---|
|  | 3104 | return $count; | 
|---|
|  | 3105 | } # end getLogCount() | 
|---|
|  | 3106 |  | 
|---|
|  | 3107 |  | 
|---|
|  | 3108 | ## DNSDB::getLogEntries() | 
|---|
|  | 3109 | # Get a list of log entries | 
|---|
|  | 3110 | # Takes arguments as with getLogCount() above, plus optional: | 
|---|
|  | 3111 | # - sort field | 
|---|
|  | 3112 | # - sort order | 
|---|
|  | 3113 | # - offset for pagination | 
|---|
|  | 3114 | sub getLogEntries { | 
|---|
|  | 3115 | my $dbh = shift; | 
|---|
|  | 3116 |  | 
|---|
|  | 3117 | my %args = @_; | 
|---|
|  | 3118 |  | 
|---|
|  | 3119 | my @filterargs; | 
|---|
|  | 3120 |  | 
|---|
|  | 3121 | # fail early if we don't have a "prime" ID to look for log entries for | 
|---|
|  | 3122 | return if !$args{id}; | 
|---|
|  | 3123 |  | 
|---|
|  | 3124 | # or if the prime id type is missing or invalid | 
|---|
|  | 3125 | return if !$args{logtype}; | 
|---|
|  | 3126 | $args{logtype} = 'revzone' if $args{logtype} eq 'rdns';       # hack pthui | 
|---|
|  | 3127 | $args{logtype} = 'domain' if $args{logtype} eq 'dom';         # hack pthui | 
|---|
|  | 3128 | return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user'); | 
|---|
|  | 3129 |  | 
|---|
|  | 3130 | # Sorting defaults | 
|---|
|  | 3131 | $args{sortby} = 'stamp' if !$args{sortby}; | 
|---|
|  | 3132 | $args{sortorder} = 'DESC' if !$args{sortorder}; | 
|---|
|  | 3133 | $args{offset} = 0 if !$args{offset}; | 
|---|
|  | 3134 |  | 
|---|
|  | 3135 | my %sortmap = (fname => 'name', username => 'email', entry => 'entry', stamp => 'stamp,log_id'); | 
|---|
|  | 3136 | $args{sortby} = $sortmap{$args{sortby}}; | 
|---|
|  | 3137 |  | 
|---|
|  | 3138 | my $sql = "SELECT user_id AS userid, email AS useremail, name AS userfname, entry AS logentry, ". | 
|---|
|  | 3139 | "date_trunc('second',stamp) AS logtime ". | 
|---|
|  | 3140 | "FROM log ". | 
|---|
|  | 3141 | "WHERE $id_col{$args{logtype}}=?". | 
|---|
|  | 3142 | ($args{filter} ? " AND entry ~* ?" : ''). | 
|---|
|  | 3143 | " ORDER BY $args{sortby} $args{sortorder}". | 
|---|
|  | 3144 | ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); | 
|---|
|  | 3145 | my $loglist = $dbh->selectall_arrayref($sql, { Slice => {} }, ($args{id}, @filterargs) ); | 
|---|
|  | 3146 | $errstr = $dbh->errstr if !$loglist; | 
|---|
|  | 3147 | return $loglist; | 
|---|
|  | 3148 | } # end getLogEntries() | 
|---|
|  | 3149 |  | 
|---|
|  | 3150 |  | 
|---|
| [225] | 3151 | ## DNSDB::getTypelist() | 
|---|
|  | 3152 | # Get a list of record types for various UI dropdowns | 
|---|
|  | 3153 | # Takes database handle, forward/reverse/lookup flag, and optional "tag as selected" indicator (defaults to A) | 
|---|
|  | 3154 | # Returns an arrayref to list of hashrefs perfect for HTML::Template | 
|---|
|  | 3155 | sub getTypelist { | 
|---|
|  | 3156 | my $dbh = shift; | 
|---|
|  | 3157 | my $recgroup = shift; | 
|---|
|  | 3158 | my $type = shift || $reverse_typemap{A}; | 
|---|
|  | 3159 |  | 
|---|
|  | 3160 | # also accepting $webvar{revrec}! | 
|---|
|  | 3161 | $recgroup = 'f' if $recgroup eq 'n'; | 
|---|
|  | 3162 | $recgroup = 'r' if $recgroup eq 'y'; | 
|---|
|  | 3163 |  | 
|---|
|  | 3164 | my $sql = "SELECT val,name FROM rectypes WHERE "; | 
|---|
|  | 3165 | if ($recgroup eq 'r') { | 
|---|
|  | 3166 | # reverse zone types | 
|---|
|  | 3167 | $sql .= "stdflag=2 OR stdflag=3"; | 
|---|
|  | 3168 | } elsif ($recgroup eq 'l') { | 
|---|
|  | 3169 | # DNS lookup types.  Note we avoid our custom types >= 65280, since those are entirely internal. | 
|---|
|  | 3170 | $sql .= "(stdflag=1 OR stdflag=2 OR stdflag=3) AND val < 65280"; | 
|---|
|  | 3171 | } else { | 
|---|
|  | 3172 | # default;  forward zone types.  technically $type eq 'f' but not worth the error message. | 
|---|
|  | 3173 | $sql .= "stdflag=1 OR stdflag=2"; | 
|---|
|  | 3174 | } | 
|---|
|  | 3175 | $sql .= " ORDER BY listorder"; | 
|---|
|  | 3176 |  | 
|---|
|  | 3177 | my $sth = $dbh->prepare($sql); | 
|---|
|  | 3178 | $sth->execute; | 
|---|
|  | 3179 | my @typelist; | 
|---|
|  | 3180 | while (my ($rval,$rname) = $sth->fetchrow_array()) { | 
|---|
|  | 3181 | my %row = ( recval => $rval, recname => $rname ); | 
|---|
|  | 3182 | $row{tselect} = 1 if $rval == $type; | 
|---|
|  | 3183 | push @typelist, \%row; | 
|---|
|  | 3184 | } | 
|---|
|  | 3185 |  | 
|---|
|  | 3186 | # Add SOA on lookups since it's not listed in other dropdowns. | 
|---|
|  | 3187 | if ($recgroup eq 'l') { | 
|---|
|  | 3188 | my %row = ( recval => $reverse_typemap{SOA}, recname => 'SOA' ); | 
|---|
|  | 3189 | $row{tselect} = 1 if $reverse_typemap{SOA} == $type; | 
|---|
|  | 3190 | push @typelist, \%row; | 
|---|
|  | 3191 | } | 
|---|
|  | 3192 |  | 
|---|
|  | 3193 | return \@typelist; | 
|---|
|  | 3194 | } # end getTypelist() | 
|---|
|  | 3195 |  | 
|---|
|  | 3196 |  | 
|---|
| [254] | 3197 | ## DNSDB::parentID() | 
|---|
|  | 3198 | # Get ID of entity that is nearest parent to requested id | 
|---|
|  | 3199 | # Takes a database handle and a hash of entity ID, entity type, optional parent type flag | 
|---|
|  | 3200 | # (domain/reverse zone or group), and optional default/live and forward/reverse flags | 
|---|
|  | 3201 | # Returns the ID or undef on failure | 
|---|
|  | 3202 | sub parentID { | 
|---|
| [116] | 3203 | my $dbh = shift; | 
|---|
|  | 3204 |  | 
|---|
| [254] | 3205 | my %args = @_; | 
|---|
| [116] | 3206 |  | 
|---|
| [254] | 3207 | # clean up the parent-type.  Set it to group if not set;  coerce revzone to domain for simpler logic | 
|---|
|  | 3208 | $args{partype} = 'group' if !$args{partype}; | 
|---|
|  | 3209 | $args{partype} = 'domain' if $args{partype} eq 'revzone'; | 
|---|
| [116] | 3210 |  | 
|---|
| [254] | 3211 | # clean up defrec and revrec.  default to live record, forward zone | 
|---|
|  | 3212 | $args{defrec} = 'n' if !$args{defrec}; | 
|---|
|  | 3213 | $args{revrec} = 'n' if !$args{revrec}; | 
|---|
| [116] | 3214 |  | 
|---|
| [254] | 3215 | if ($par_type{$args{partype}} eq 'domain') { | 
|---|
|  | 3216 | # only live records can have a domain/zone parent | 
|---|
|  | 3217 | return unless ($args{type} eq 'record' && $args{defrec} eq 'n'); | 
|---|
|  | 3218 | my $result = $dbh->selectrow_hashref("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id'). | 
|---|
|  | 3219 | " FROM records WHERE record_id = ?", | 
|---|
|  | 3220 | undef, ($args{id}) ) or return; | 
|---|
|  | 3221 | return $result; | 
|---|
|  | 3222 | } else { | 
|---|
|  | 3223 | # snag some arguments that will either fall through or be overwritten to save some code duplication | 
|---|
|  | 3224 | my $tmpid = $args{id}; | 
|---|
|  | 3225 | my $type = $args{type}; | 
|---|
|  | 3226 | if ($type eq 'record' && $args{defrec} eq 'n') { | 
|---|
|  | 3227 | # Live records go through the records table first. | 
|---|
|  | 3228 | ($tmpid) = $dbh->selectrow_array("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id'). | 
|---|
|  | 3229 | " FROM records WHERE record_id = ?", | 
|---|
|  | 3230 | undef, ($args{id}) ) or return; | 
|---|
|  | 3231 | $type = ($args{revrec} eq 'n' ? 'domain' : 'revzone'); | 
|---|
|  | 3232 | } | 
|---|
|  | 3233 | my ($result) = $dbh->selectrow_array("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?", | 
|---|
|  | 3234 | undef, ($tmpid) ); | 
|---|
|  | 3235 | return $result; | 
|---|
|  | 3236 | } | 
|---|
|  | 3237 | # should be impossible to get here with even remotely sane arguments | 
|---|
|  | 3238 | return; | 
|---|
|  | 3239 | } # end parentID() | 
|---|
| [116] | 3240 |  | 
|---|
|  | 3241 |  | 
|---|
| [117] | 3242 | ## DNSDB::isParent() | 
|---|
|  | 3243 | # Returns true if $id1 is a parent of $id2, false otherwise | 
|---|
|  | 3244 | sub isParent { | 
|---|
|  | 3245 | my $dbh = shift; | 
|---|
|  | 3246 | my $id1 = shift; | 
|---|
|  | 3247 | my $type1 = shift; | 
|---|
|  | 3248 | my $id2 = shift; | 
|---|
|  | 3249 | my $type2 = shift; | 
|---|
|  | 3250 | ##todo:  immediate, secondary, full (default) | 
|---|
|  | 3251 |  | 
|---|
| [157] | 3252 | # Return false on invalid types | 
|---|
| [244] | 3253 | return 0 if !grep /^$type1$/, ('record','defrec','defrevrec','user','domain','revzone','group'); | 
|---|
|  | 3254 | return 0 if !grep /^$type2$/, ('record','defrec','defrevrec','user','domain','revzone','group'); | 
|---|
| [157] | 3255 |  | 
|---|
| [117] | 3256 | # Return false on impossible relations | 
|---|
|  | 3257 | return 0 if $type1 eq 'record';       # nothing may be a child of a record | 
|---|
|  | 3258 | return 0 if $type1 eq 'defrec';       # nothing may be a child of a record | 
|---|
| [244] | 3259 | return 0 if $type1 eq 'defrevrec';    # nothing may be a child of a record | 
|---|
| [117] | 3260 | return 0 if $type1 eq 'user';         # nothing may be child of a user | 
|---|
|  | 3261 | return 0 if $type1 eq 'domain' && $type2 ne 'record'; # domain may not be a parent of anything other than a record | 
|---|
| [244] | 3262 | return 0 if $type1 eq 'revzone' && $type2 ne 'record';# reverse zone may not be a parent of anything other than a record | 
|---|
| [117] | 3263 |  | 
|---|
| [186] | 3264 | # ennnhhhh....  if we're passed an id of 0, it will never be found.  usual | 
|---|
|  | 3265 | # case would be the UI creating a new <thing>, and so we don't have an ID for | 
|---|
|  | 3266 | # <thing> to look up yet.  in that case the UI should check the parent as well. | 
|---|
|  | 3267 | return 0 if $id1 == 0;        # nothing can have a parent id of 0 | 
|---|
|  | 3268 | return 1 if $id2 == 0;        # anything could have a child id of 0 (or "unknown") | 
|---|
|  | 3269 |  | 
|---|
| [117] | 3270 | # group 1 is the ultimate root parent | 
|---|
|  | 3271 | return 1 if $type1 eq 'group' && $id1 == 1; | 
|---|
|  | 3272 |  | 
|---|
| [155] | 3273 | # groups are always (a) parent of themselves | 
|---|
|  | 3274 | return 1 if $type1 eq 'group' && $type2 eq 'group' && $id1 == $id2; | 
|---|
|  | 3275 |  | 
|---|
| [117] | 3276 | my $id = $id2; | 
|---|
|  | 3277 | my $type = $type2; | 
|---|
|  | 3278 | my $foundparent = 0; | 
|---|
| [155] | 3279 |  | 
|---|
| [244] | 3280 | # Records are the only entity with two possible parents.  We need to split the parent checks on | 
|---|
|  | 3281 | # domain/rdns. | 
|---|
|  | 3282 | if ($type eq 'record') { | 
|---|
|  | 3283 | my ($dom,$rdns) = $dbh->selectrow_array("SELECT domain_id,rdns_id FROM records WHERE record_id=?", | 
|---|
|  | 3284 | undef, ($id)); | 
|---|
|  | 3285 | # check immediate parent against request | 
|---|
|  | 3286 | return 1 if $type1 eq 'domain' && $id1 == $dom; | 
|---|
|  | 3287 | return 1 if $type1 eq 'revzone' && $id1 == $rdns; | 
|---|
|  | 3288 | # if request is group, check *both* parents.  Only check if the parent is nonzero though. | 
|---|
|  | 3289 | return 1 if $dom && isParent($dbh, $id1, $type1, $dom, 'domain'); | 
|---|
|  | 3290 | return 1 if $rdns && isParent($dbh, $id1, $type1, $rdns, 'revzone'); | 
|---|
|  | 3291 | # exit here since we've executed the loop below by proxy in the above recursive calls. | 
|---|
|  | 3292 | return 0; | 
|---|
|  | 3293 | } | 
|---|
|  | 3294 |  | 
|---|
|  | 3295 | # almost the same loop as getParents() above | 
|---|
| [186] | 3296 | my $limiter = 0; | 
|---|
| [117] | 3297 | while (1) { | 
|---|
| [155] | 3298 | my $sql = "SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?"; | 
|---|
| [117] | 3299 | my $result = $dbh->selectrow_hashref($sql, | 
|---|
| [157] | 3300 | undef, ($id) ); | 
|---|
| [186] | 3301 | if (!$result) { | 
|---|
|  | 3302 | $limiter++; | 
|---|
| [244] | 3303 | ##fixme:  how often will this happen on a live site?  fail at max limiter <n>? | 
|---|
| [186] | 3304 | warn "no results looking for $sql with id $id (depth $limiter)\n"; | 
|---|
|  | 3305 | last; | 
|---|
|  | 3306 | } | 
|---|
| [157] | 3307 | if ($result && $result->{$par_col{$type}} == $id1) { | 
|---|
| [117] | 3308 | $foundparent = 1; | 
|---|
|  | 3309 | last; | 
|---|
| [157] | 3310 | } else { | 
|---|
|  | 3311 | ##fixme: do we care about trying to return a "no such record/domain/user/group" error? | 
|---|
| [244] | 3312 | # should be impossible to create an inconsistent DB just with API calls. | 
|---|
| [157] | 3313 | warn $dbh->errstr." $sql, $id" if $dbh->errstr; | 
|---|
| [117] | 3314 | } | 
|---|
|  | 3315 | # group 1 is its own parent.  need this here more to break strange loops than for detecting a parent | 
|---|
|  | 3316 | last if $result->{$par_col{$type}} == 1; | 
|---|
| [152] | 3317 | $id = $result->{$par_col{$type}}; | 
|---|
| [117] | 3318 | $type = $par_type{$type}; | 
|---|
|  | 3319 | } | 
|---|
|  | 3320 |  | 
|---|
|  | 3321 | return $foundparent; | 
|---|
|  | 3322 | } # end isParent() | 
|---|
|  | 3323 |  | 
|---|
|  | 3324 |  | 
|---|
| [275] | 3325 | ## DNSDB::zoneStatus() | 
|---|
|  | 3326 | # Returns and optionally sets a zone's status | 
|---|
|  | 3327 | # Takes a database handle, domain/revzone ID, forward/reverse flag, and optionally a status argument | 
|---|
|  | 3328 | # Returns status, or undef on errors. | 
|---|
|  | 3329 | sub zoneStatus { | 
|---|
| [3] | 3330 | my $dbh = shift; | 
|---|
|  | 3331 | my $id = shift; | 
|---|
| [275] | 3332 | my $revrec = shift; | 
|---|
|  | 3333 | my $newstatus = shift || 'mu'; | 
|---|
| [3] | 3334 |  | 
|---|
|  | 3335 | return undef if $id !~ /^\d+$/; | 
|---|
|  | 3336 |  | 
|---|
| [283] | 3337 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 3338 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 3339 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 3340 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 3341 |  | 
|---|
| [275] | 3342 | if ($newstatus ne 'mu') { | 
|---|
| [283] | 3343 | # ooo, fun!  let's see what we were passed for status | 
|---|
|  | 3344 | eval { | 
|---|
|  | 3345 | $newstatus = 0 if $newstatus eq 'domoff'; | 
|---|
|  | 3346 | $newstatus = 1 if $newstatus eq 'domon'; | 
|---|
|  | 3347 | $dbh->do("UPDATE ".($revrec eq 'n' ? 'domains' : 'revzones')." SET status=? WHERE ". | 
|---|
| [275] | 3348 | ($revrec eq 'n' ? 'domain_id' : 'rdns_id')."=?", undef, ($newstatus,$id) ); | 
|---|
| [283] | 3349 |  | 
|---|
|  | 3350 | ##fixme  switch to more consise "Enabled <domain"/"Disabled <domain>" as with users? | 
|---|
|  | 3351 | $resultstr = "Changed ".($revrec eq 'n' ? domainName($dbh, $id) : revName($dbh, $id)). | 
|---|
|  | 3352 | " state to ".($newstatus ? 'active' : 'inactive'); | 
|---|
|  | 3353 |  | 
|---|
|  | 3354 | my %loghash; | 
|---|
|  | 3355 | $loghash{domain_id} = $id if $revrec eq 'n'; | 
|---|
|  | 3356 | $loghash{rdns_id} = $id if $revrec eq 'y'; | 
|---|
|  | 3357 | $loghash{group_id} = parentID($dbh, | 
|---|
|  | 3358 | (id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ); | 
|---|
|  | 3359 | $loghash{entry} = $resultstr; | 
|---|
|  | 3360 | _log($dbh, %loghash); | 
|---|
|  | 3361 |  | 
|---|
|  | 3362 | $dbh->commit; | 
|---|
|  | 3363 | }; | 
|---|
|  | 3364 | if ($@) { | 
|---|
|  | 3365 | my $msg = $@; | 
|---|
|  | 3366 | eval { $dbh->rollback; }; | 
|---|
|  | 3367 | $resultstr = ''; | 
|---|
|  | 3368 | $errstr = $msg; | 
|---|
|  | 3369 | return; | 
|---|
|  | 3370 | } | 
|---|
| [3] | 3371 | } | 
|---|
|  | 3372 |  | 
|---|
| [275] | 3373 | my ($status) = $dbh->selectrow_array("SELECT status FROM ". | 
|---|
|  | 3374 | ($revrec eq 'n' ? "domains WHERE domain_id=?" : "revzones WHERE rdns_id=?"), | 
|---|
|  | 3375 | undef, ($id) ); | 
|---|
| [3] | 3376 | return $status; | 
|---|
| [275] | 3377 | } # end zoneStatus() | 
|---|
| [3] | 3378 |  | 
|---|
|  | 3379 |  | 
|---|
| [33] | 3380 | ## DNSDB::importAXFR | 
|---|
|  | 3381 | # Import a domain via AXFR | 
|---|
| [37] | 3382 | # Takes AXFR host, domain to transfer, group to put the domain in, | 
|---|
|  | 3383 | # and optionally: | 
|---|
|  | 3384 | #  - active/inactive state flag (defaults to active) | 
|---|
|  | 3385 | #  - overwrite-SOA flag (defaults to off) | 
|---|
|  | 3386 | #  - overwrite-NS flag (defaults to off, doesn't affect subdomain NS records) | 
|---|
|  | 3387 | # Returns a status code (OK, WARN, or FAIL) and message - message should be blank | 
|---|
|  | 3388 | # if status is OK, but WARN includes conditions that are not fatal but should | 
|---|
|  | 3389 | # really be reported. | 
|---|
| [33] | 3390 | sub importAXFR { | 
|---|
|  | 3391 | my $dbh = shift; | 
|---|
| [35] | 3392 | my $ifrom_in = shift; | 
|---|
| [301] | 3393 | my $zone = shift; | 
|---|
| [33] | 3394 | my $group = shift; | 
|---|
|  | 3395 | my $status = shift || 1; | 
|---|
|  | 3396 | my $rwsoa = shift || 0; | 
|---|
|  | 3397 | my $rwns = shift || 0; | 
|---|
| [301] | 3398 | my $merge = shift || 0;       # do we attempt to merge A/AAAA and PTR records whenever possible? | 
|---|
|  | 3399 | # do we overload this with the fixme below? | 
|---|
| [33] | 3400 | ##fixme:  add mode to delete&replace, merge+overwrite, merge new? | 
|---|
|  | 3401 |  | 
|---|
| [37] | 3402 | my $nrecs = 0; | 
|---|
|  | 3403 | my $soaflag = 0; | 
|---|
|  | 3404 | my $nsflag = 0; | 
|---|
|  | 3405 | my $warnmsg = ''; | 
|---|
|  | 3406 | my $ifrom; | 
|---|
| [33] | 3407 |  | 
|---|
| [301] | 3408 | my $rev = 'n'; | 
|---|
| [302] | 3409 | my $code = 'OK'; | 
|---|
|  | 3410 | my $msg = 'foobar?'; | 
|---|
| [301] | 3411 |  | 
|---|
| [35] | 3412 | # choke on possible bad setting in ifrom | 
|---|
| [37] | 3413 | # IPv4 and v6, and valid hostnames! | 
|---|
| [35] | 3414 | ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i); | 
|---|
|  | 3415 | return ('FAIL', "Bad AXFR source host $ifrom") | 
|---|
|  | 3416 | unless ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i); | 
|---|
|  | 3417 |  | 
|---|
| [301] | 3418 | my $errmsg; | 
|---|
|  | 3419 |  | 
|---|
|  | 3420 | my $zone_id; | 
|---|
|  | 3421 | my $domain_id = 0; | 
|---|
|  | 3422 | my $rdns_id = 0; | 
|---|
|  | 3423 | my $cidr; | 
|---|
|  | 3424 |  | 
|---|
|  | 3425 | # magic happens!  detect if we're importing a domain or a reverse zone | 
|---|
|  | 3426 | # while we're at it, figure out what the CIDR netblock is (if we got a .arpa) | 
|---|
|  | 3427 | # or what the formal .arpa zone is (if we got a CIDR netblock) | 
|---|
|  | 3428 | # Handles sub-octet v4 zones in the format specified in the Cricket Book, 2nd Ed, p217-218 | 
|---|
|  | 3429 |  | 
|---|
|  | 3430 | if ($zone =~ m{(?:\.arpa\.?|/\d+)$}) { | 
|---|
|  | 3431 | # we seem to have a reverse zone | 
|---|
|  | 3432 | $rev = 'y'; | 
|---|
|  | 3433 |  | 
|---|
|  | 3434 | if ($zone =~ /\.arpa\.?$/) { | 
|---|
|  | 3435 | # we have a formal reverse zone.  call _zone2cidr and get the CIDR block. | 
|---|
|  | 3436 | ($code,$msg) = _zone2cidr($zone); | 
|---|
|  | 3437 | return ($code, $msg) if $code eq 'FAIL'; | 
|---|
|  | 3438 | $cidr = $msg; | 
|---|
|  | 3439 | } elsif ($zone =~ m|^[\d.]+/\d+$|) { | 
|---|
|  | 3440 | # v4 revzone, CIDR netblock | 
|---|
|  | 3441 | $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block"); | 
|---|
|  | 3442 | $zone = _ZONE($cidr, 'ZONE.in-addr.arpa', 'r', '.'); | 
|---|
|  | 3443 | } elsif ($zone =~ m|^[a-fA-F\d:]+/\d+$|) { | 
|---|
|  | 3444 | # v6 revzone, CIDR netblock | 
|---|
|  | 3445 | $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block"); | 
|---|
|  | 3446 | return ('FAIL', "$zone is not a nibble-aligned block") if $cidr->masklen % 4 != 0; | 
|---|
|  | 3447 | $zone = _ZONE($cidr, 'ZONE.ip6.arpa', 'r', '.'); | 
|---|
|  | 3448 | } else { | 
|---|
|  | 3449 | # there is. no. else! | 
|---|
|  | 3450 | return ('FAIL', "Unknown zone name format"); | 
|---|
|  | 3451 | } | 
|---|
|  | 3452 |  | 
|---|
|  | 3453 | # quick check to start to see if we've already got one | 
|---|
|  | 3454 |  | 
|---|
|  | 3455 | ($zone_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", | 
|---|
|  | 3456 | undef, ("$cidr")); | 
|---|
|  | 3457 | $rdns_id = $zone_id; | 
|---|
|  | 3458 | } else { | 
|---|
|  | 3459 | # default to domain | 
|---|
|  | 3460 | ($zone_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", | 
|---|
|  | 3461 | undef, ($zone)); | 
|---|
|  | 3462 | $domain_id = $zone_id; | 
|---|
|  | 3463 | } | 
|---|
|  | 3464 |  | 
|---|
|  | 3465 | return ('FAIL', ($rev eq 'n' ? 'Domain' : 'Reverse zone')." already exists") if $zone_id; | 
|---|
|  | 3466 |  | 
|---|
| [303] | 3467 | # little local utility sub to swap $val and $host for revzone records. | 
|---|
|  | 3468 | sub _revswap { | 
|---|
|  | 3469 | my $rechost = shift; | 
|---|
|  | 3470 | my $recdata = shift; | 
|---|
|  | 3471 |  | 
|---|
|  | 3472 | if ($rechost =~ /\.in-addr\.arpa\.?$/) { | 
|---|
|  | 3473 | $rechost =~ s/\.in-addr\.arpa\.?$//; | 
|---|
|  | 3474 | $rechost = join '.', reverse split /\./, $rechost; | 
|---|
|  | 3475 | } else { | 
|---|
|  | 3476 | $rechost =~ s/\.ip6\.arpa\.?$//; | 
|---|
|  | 3477 | my @nibs = reverse split /\./, $rechost; | 
|---|
|  | 3478 | $rechost = ''; | 
|---|
|  | 3479 | my $nc; | 
|---|
|  | 3480 | foreach (@nibs) { | 
|---|
|  | 3481 | $rechost.= $_; | 
|---|
|  | 3482 | $rechost .= ":" if ++$nc % 4 == 0 && $nc < 32; | 
|---|
|  | 3483 | } | 
|---|
| [307] | 3484 | $rechost .= ":" if $nc < 32 && $rechost !~ /\*$/; # close netblock records? | 
|---|
|  | 3485 | ##fixme:  there's a case that ends up with a partial entry here: | 
|---|
|  | 3486 | # ip:add:re:ss:: | 
|---|
|  | 3487 | # can't reproduce after letting it sit overnight after discovery.  :( | 
|---|
|  | 3488 | #print "$rechost\n"; | 
|---|
| [303] | 3489 | # canonicalize with NetAddr::IP | 
|---|
|  | 3490 | $rechost = NetAddr::IP->new($rechost)->addr unless $rechost =~ /\*$/; | 
|---|
|  | 3491 | } | 
|---|
|  | 3492 | return ($recdata,$rechost) | 
|---|
|  | 3493 | } | 
|---|
|  | 3494 |  | 
|---|
|  | 3495 |  | 
|---|
| [33] | 3496 | # Allow transactions, and raise an exception on errors so we can catch it later. | 
|---|
|  | 3497 | # Use local to make sure these get "reset" properly on exiting this block | 
|---|
|  | 3498 | local $dbh->{AutoCommit} = 0; | 
|---|
|  | 3499 | local $dbh->{RaiseError} = 1; | 
|---|
|  | 3500 |  | 
|---|
| [301] | 3501 | my $sth; | 
|---|
|  | 3502 | eval { | 
|---|
| [34] | 3503 |  | 
|---|
| [301] | 3504 | if ($rev eq 'n') { | 
|---|
| [33] | 3505 | ##fixme:  serial | 
|---|
| [301] | 3506 | $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($zone,$group,$status) ); | 
|---|
|  | 3507 | # get domain id so we can do the records | 
|---|
|  | 3508 | ($zone_id) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')"); | 
|---|
|  | 3509 | $domain_id = $zone_id; | 
|---|
|  | 3510 | _log($dbh, (group_id => $group, domain_id => $domain_id, | 
|---|
|  | 3511 | entry => "[Added ".($status ? 'active' : 'inactive')." domain $zone via AXFR]") ); | 
|---|
|  | 3512 | } else { | 
|---|
|  | 3513 | ##fixme:  serial | 
|---|
|  | 3514 | $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($cidr,$group,$status) ); | 
|---|
|  | 3515 | # get revzone id so we can do the records | 
|---|
|  | 3516 | ($zone_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); | 
|---|
|  | 3517 | $rdns_id = $zone_id; | 
|---|
|  | 3518 | _log($dbh, (group_id => $group, rdns_id => $rdns_id, | 
|---|
| [303] | 3519 | entry => "[Added ".($status ? 'active' : 'inactive')." reverse zone $cidr via AXFR]") ); | 
|---|
| [301] | 3520 | } | 
|---|
| [33] | 3521 |  | 
|---|
| [35] | 3522 | ## bizarre DBI<->Net::DNS interaction bug: | 
|---|
|  | 3523 | ## sometimes a zone will cause an immediate commit-and-exit (sort of) of the while() | 
|---|
| [37] | 3524 | ## fixed, apparently I was doing *something* odd, but not certain what it was that | 
|---|
|  | 3525 | ## caused a commit instead of barfing | 
|---|
| [35] | 3526 |  | 
|---|
| [34] | 3527 | my $res = Net::DNS::Resolver->new; | 
|---|
| [35] | 3528 | $res->nameservers($ifrom); | 
|---|
| [301] | 3529 | $res->axfr_start($zone) | 
|---|
| [35] | 3530 | or die "Couldn't begin AXFR\n"; | 
|---|
| [34] | 3531 |  | 
|---|
| [301] | 3532 | $sth = $dbh->prepare("INSERT INTO records (domain_id,rdns_id,host,type,val,distance,weight,port,ttl)". | 
|---|
|  | 3533 | " VALUES (?,?,?,?,?,?,?,?,?)"); | 
|---|
|  | 3534 |  | 
|---|
| [307] | 3535 | # Stash info about sub-octet v4 revzones here so we don't have | 
|---|
|  | 3536 | # to store the CNAMEs used to delegate a suboctet zone | 
|---|
|  | 3537 | # $suboct{zone}{ns}[] -> array of nameservers | 
|---|
|  | 3538 | # $suboct{zone}{cname}[] -> array of extant CNAMEs (Just In Case someone did something bizarre) | 
|---|
|  | 3539 | ## commented pending actual use of this data.  for now, we'll just | 
|---|
|  | 3540 | ## auto-(re)create the CNAMEs in revzones on export | 
|---|
|  | 3541 | #    my %suboct; | 
|---|
|  | 3542 |  | 
|---|
| [35] | 3543 | while (my $rr = $res->axfr_next()) { | 
|---|
| [301] | 3544 |  | 
|---|
|  | 3545 | my $val; | 
|---|
|  | 3546 | my $distance = 0; | 
|---|
|  | 3547 | my $weight = 0; | 
|---|
|  | 3548 | my $port = 0; | 
|---|
| [303] | 3549 | my $logfrag = ''; | 
|---|
| [301] | 3550 |  | 
|---|
| [33] | 3551 | my $type = $rr->type; | 
|---|
| [301] | 3552 | my $host = $rr->name; | 
|---|
|  | 3553 | my $ttl = $rr->ttl; | 
|---|
| [35] | 3554 |  | 
|---|
| [37] | 3555 | $soaflag = 1 if $type eq 'SOA'; | 
|---|
|  | 3556 | $nsflag = 1 if $type eq 'NS'; | 
|---|
| [35] | 3557 |  | 
|---|
| [34] | 3558 | # "Primary" types: | 
|---|
|  | 3559 | # A, NS, CNAME, SOA, PTR(warn in forward), MX, TXT, AAAA, SRV, A6(ob), SPF | 
|---|
|  | 3560 | # maybe KEY | 
|---|
|  | 3561 |  | 
|---|
| [302] | 3562 | # BIND supports: | 
|---|
|  | 3563 | # [standard] | 
|---|
|  | 3564 | # A AAAA CNAME MX NS PTR SOA TXT | 
|---|
|  | 3565 | # [variously experimental, obsolete, or obscure] | 
|---|
|  | 3566 | # HINFO MB(ex) MD(ob) MF(ob) MG(ex) MINFO(ex) MR(ex) NULL WKS AFSDB(ex) ISDN(ex) RP(ex) RT(ex) X25(ex) PX | 
|---|
|  | 3567 | # ... if one can ever find the right magic to format them correctly | 
|---|
|  | 3568 |  | 
|---|
|  | 3569 | # Net::DNS supports: | 
|---|
|  | 3570 | # RRSIG SIG NSAP NS NIMLOC NAPTR MX MR MINFO MG MB LOC ISDN IPSECKEY HINFO | 
|---|
|  | 3571 | # EID DNAME CNAME CERT APL AFSDB AAAA A DS NXT NSEC3PARAM NSEC3 NSEC KEY | 
|---|
|  | 3572 | # DNSKEY DLV X25 TXT TSIG TKEY SSHFP SRV SPF SOA RT RP PX PTR NULL APL::AplItem | 
|---|
|  | 3573 |  | 
|---|
| [35] | 3574 | # nasty big ugly case-like thing here, since we have to do *some* different | 
|---|
|  | 3575 | # processing depending on the record.  le sigh. | 
|---|
|  | 3576 |  | 
|---|
| [105] | 3577 | ##fixme:  what record types other than TXT can/will have >255-byte payloads? | 
|---|
|  | 3578 |  | 
|---|
| [34] | 3579 | if ($type eq 'A') { | 
|---|
| [301] | 3580 | $val = $rr->address; | 
|---|
| [34] | 3581 | } elsif ($type eq 'NS') { | 
|---|
| [37] | 3582 | # hmm.  should we warn here if subdomain NS'es are left alone? | 
|---|
| [301] | 3583 | next if ($rwns && ($rr->name eq $zone)); | 
|---|
| [302] | 3584 | if ($rev eq 'y') { | 
|---|
|  | 3585 | # revzones have records more or less reversed from forward zones. | 
|---|
|  | 3586 | my ($tmpcode,$tmpmsg) = _zone2cidr($host); | 
|---|
|  | 3587 | die "Error converting NS record: $tmpmsg" if $tmpcode eq 'FAIL';      # hmm.  may not make sense... | 
|---|
|  | 3588 | $val = "$tmpmsg"; | 
|---|
|  | 3589 | $host = $rr->nsdname; | 
|---|
| [303] | 3590 | $logfrag = "Added record '$val $type $host', TTL $ttl"; | 
|---|
| [307] | 3591 | # Tag and preserve.  For now this is commented for a no-op, but we have Ideas for | 
|---|
|  | 3592 | # another custom storage type ("DELEGATE") that will use these subzone-delegation records | 
|---|
|  | 3593 | #if ($val ne "$cidr") { | 
|---|
|  | 3594 | #  push @{$suboct{$val}{ns}}, $host; | 
|---|
|  | 3595 | #} | 
|---|
| [302] | 3596 | } else { | 
|---|
|  | 3597 | $val = $rr->nsdname; | 
|---|
|  | 3598 | } | 
|---|
| [35] | 3599 | $nsflag = 1; | 
|---|
| [34] | 3600 | } elsif ($type eq 'CNAME') { | 
|---|
| [302] | 3601 | if ($rev eq 'y') { | 
|---|
| [303] | 3602 | # hmm.  do we even want to bother with storing these at this level?  Sub-octet delegation | 
|---|
|  | 3603 | # by CNAME is essentially a record-publication hack, and we want to just represent the | 
|---|
|  | 3604 | # "true" logical intentions as far down the stack as we can from the UI. | 
|---|
|  | 3605 | ($host,$val) = _revswap($host,$rr->cname); | 
|---|
|  | 3606 | $logfrag = "Added record '$val $type $host', TTL $ttl"; | 
|---|
| [307] | 3607 | # Tag and preserve in case we want to commit them as-is later, but mostly we don't care. | 
|---|
|  | 3608 | # Commented pending actually doing something with possibly new type DELEGATE | 
|---|
|  | 3609 | #my $tmprev = $host; | 
|---|
|  | 3610 | #$tmprev =~ s/^\d+\.//; | 
|---|
|  | 3611 | #($code,$tmprev) = _zone2cidr($tmprev); | 
|---|
|  | 3612 | #push @{$suboct{"$tmprev"}{cname}}, $val; | 
|---|
|  | 3613 | # Silently skip CNAMEs in revzones. | 
|---|
|  | 3614 | next; | 
|---|
| [302] | 3615 | } else { | 
|---|
|  | 3616 | $val = $rr->cname; | 
|---|
|  | 3617 | } | 
|---|
| [34] | 3618 | } elsif ($type eq 'SOA') { | 
|---|
| [37] | 3619 | next if $rwsoa; | 
|---|
| [307] | 3620 | $host = $rr->rname.":".$rr->mname; | 
|---|
| [301] | 3621 | $val = $rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum; | 
|---|
| [35] | 3622 | $soaflag = 1; | 
|---|
| [34] | 3623 | } elsif ($type eq 'PTR') { | 
|---|
| [303] | 3624 | ($host,$val) = _revswap($host,$rr->ptrdname); | 
|---|
|  | 3625 | $logfrag = "Added record '$val $type $host', TTL $ttl"; | 
|---|
| [34] | 3626 | # hmm.  PTR records should not be in forward zones. | 
|---|
|  | 3627 | } elsif ($type eq 'MX') { | 
|---|
| [301] | 3628 | $val = $rr->exchange; | 
|---|
|  | 3629 | $distance = $rr->preference; | 
|---|
| [34] | 3630 | } elsif ($type eq 'TXT') { | 
|---|
|  | 3631 | ##fixme:  Net::DNS docs say this should be deprecated for rdatastr() or char_str_list(), | 
|---|
|  | 3632 | ## but don't really seem enthusiastic about it. | 
|---|
| [303] | 3633 | #print "should use rdatastr:\n\t".$rr->rdatastr."\n  or char_str_list:\n\t".join(' ',$rr->char_str_list())."\n"; | 
|---|
|  | 3634 | # rdatastr returns a BIND-targetted logical string, including opening and closing quotes | 
|---|
|  | 3635 | # char_str_list returns a list of the individual string fragments in the record | 
|---|
| [307] | 3636 | # txtdata returns the more useful all-in-one form (since we want to push such protocol | 
|---|
|  | 3637 | # details as far down the stack as we can) | 
|---|
| [303] | 3638 | # NB:  this may turn out to be more troublesome if we ever have need of >512-byte TXT records. | 
|---|
|  | 3639 | if ($rev eq 'y') { | 
|---|
|  | 3640 | ($host,$val) = _revswap($host,$rr->txtdata); | 
|---|
|  | 3641 | $logfrag = "Added record '$val $type $host', TTL $ttl"; | 
|---|
|  | 3642 | } else { | 
|---|
|  | 3643 | $val = $rr->txtdata; | 
|---|
|  | 3644 | } | 
|---|
| [34] | 3645 | } elsif ($type eq 'SPF') { | 
|---|
|  | 3646 | ##fixme: and the same caveat here, since it is apparently a clone of ::TXT | 
|---|
| [301] | 3647 | $val = $rr->txtdata; | 
|---|
| [34] | 3648 | } elsif ($type eq 'AAAA') { | 
|---|
| [301] | 3649 | $val = $rr->address; | 
|---|
| [34] | 3650 | } elsif ($type eq 'SRV') { | 
|---|
| [301] | 3651 | $val = $rr->target; | 
|---|
|  | 3652 | $distance = $rr->priority; | 
|---|
|  | 3653 | $weight = $rr->weight; | 
|---|
|  | 3654 | $port = $rr->port; | 
|---|
| [34] | 3655 | } elsif ($type eq 'KEY') { | 
|---|
| [35] | 3656 | # we don't actually know what to do with these... | 
|---|
| [301] | 3657 | $val = $rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname; | 
|---|
| [35] | 3658 | } else { | 
|---|
| [301] | 3659 | $val = $rr->rdatastr; | 
|---|
| [35] | 3660 | # Finding a different record type is not fatal.... just problematic. | 
|---|
| [37] | 3661 | # We may not be able to export it correctly. | 
|---|
| [35] | 3662 | $warnmsg .= "Unusual record ".$rr->name." ($type) found\n"; | 
|---|
| [33] | 3663 | } | 
|---|
|  | 3664 |  | 
|---|
| [303] | 3665 | my $logentry = "[AXFR ".($rev eq 'n' ? $zone : $cidr)."] "; | 
|---|
| [34] | 3666 |  | 
|---|
| [307] | 3667 | if ($merge) { | 
|---|
|  | 3668 | if ($rev eq 'n') { | 
|---|
|  | 3669 | # importing a domain;  we have A and AAAA records that could be merged with matching PTR records | 
|---|
|  | 3670 | my $etype; | 
|---|
|  | 3671 | my ($erdns,$erid,$ettl) = $dbh->selectrow_array("SELECT rdns_id,record_id,ttl FROM records ". | 
|---|
|  | 3672 | "WHERE host=? AND val=? AND type=12", | 
|---|
|  | 3673 | undef, ($host, $val) ); | 
|---|
|  | 3674 | if ($erid) { | 
|---|
|  | 3675 | if ($type eq 'A') { # PTR -> A+PTR | 
|---|
|  | 3676 | $etype = 65280; | 
|---|
|  | 3677 | $logentry .= "Merged A record with existing PTR record '$host A+PTR $val', TTL $ettl"; | 
|---|
|  | 3678 | } | 
|---|
|  | 3679 | if ($type eq 'AAAA') {      # PTR -> AAAA+PTR | 
|---|
|  | 3680 | $etype = 65281; | 
|---|
|  | 3681 | $logentry .= "Merged AAAA record with existing PTR record '$host AAAA+PTR $val', TTL $ettl"; | 
|---|
|  | 3682 | } | 
|---|
|  | 3683 | $ettl = ($ettl < $ttl ? $ettl : $ttl);    # use lower TTL | 
|---|
|  | 3684 | $dbh->do("UPDATE records SET domain_id=?,ttl=?,type=? WHERE record_id=?", undef, | 
|---|
|  | 3685 | ($domain_id, $ettl, $etype, $erid)); | 
|---|
|  | 3686 | $nrecs++; | 
|---|
|  | 3687 | _log($dbh, (group_id => $group, domain_id => $domain_id, rdns_id => $erdns, entry => $logentry) ); | 
|---|
|  | 3688 | next;       # while axfr_next | 
|---|
|  | 3689 | } | 
|---|
|  | 3690 | } # $rev eq 'n' | 
|---|
|  | 3691 | else { | 
|---|
|  | 3692 | # importing a revzone, we have PTR records that could be merged with matching A/AAAA records | 
|---|
|  | 3693 | my ($domid,$erid,$ettl,$etype) = $dbh->selectrow_array("SELECT domain_id,record_id,ttl,type FROM records ". | 
|---|
|  | 3694 | "WHERE host=? AND val=? AND (type=1 OR type=28)", | 
|---|
|  | 3695 | undef, ($host, $val) ); | 
|---|
|  | 3696 | if ($erid) { | 
|---|
|  | 3697 | if ($etype == 1) {  # A -> A+PTR | 
|---|
|  | 3698 | $etype = 65280; | 
|---|
|  | 3699 | $logentry .= "Merged PTR record with existing matching A record '$host A+PTR $val', TTL $ettl"; | 
|---|
|  | 3700 | } | 
|---|
|  | 3701 | if ($etype == 28) { # AAAA -> AAAA+PTR | 
|---|
|  | 3702 | $etype = 65281; | 
|---|
|  | 3703 | $logentry .= "Merged PTR record with existing matching AAAA record '$host AAAA+PTR $val', TTL $ettl"; | 
|---|
|  | 3704 | } | 
|---|
|  | 3705 | $ettl = ($ettl < $ttl ? $ettl : $ttl);    # use lower TTL | 
|---|
|  | 3706 | $dbh->do("UPDATE records SET rdns_id=?,ttl=?,type=? WHERE record_id=?", undef, | 
|---|
|  | 3707 | ($rdns_id, $ettl, $etype, $erid)); | 
|---|
|  | 3708 | $nrecs++; | 
|---|
|  | 3709 | _log($dbh, (group_id => $group, domain_id => $domid, rdns_id => $rdns_id, entry => $logentry) ); | 
|---|
|  | 3710 | next;       # while axfr_next | 
|---|
|  | 3711 | } | 
|---|
|  | 3712 | } # $rev eq 'y' | 
|---|
|  | 3713 | } # if $merge | 
|---|
| [34] | 3714 |  | 
|---|
| [302] | 3715 | # Insert the new record | 
|---|
| [301] | 3716 | $sth->execute($domain_id, $rdns_id, $host, $reverse_typemap{$type}, $val, | 
|---|
|  | 3717 | $distance, $weight, $port, $ttl); | 
|---|
|  | 3718 |  | 
|---|
| [37] | 3719 | $nrecs++; | 
|---|
| [34] | 3720 |  | 
|---|
| [301] | 3721 | if ($type eq 'SOA') { | 
|---|
|  | 3722 | # also !$rwsoa, but if that's set, it should be impossible to get here. | 
|---|
|  | 3723 | my @tmp1 = split /:/, $host; | 
|---|
|  | 3724 | my @tmp2 = split /:/, $val; | 
|---|
|  | 3725 | $logentry .= "Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". | 
|---|
|  | 3726 | "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl"; | 
|---|
| [303] | 3727 | } elsif ($logfrag) { | 
|---|
|  | 3728 | # special case for log entries we need to meddle with a little. | 
|---|
|  | 3729 | $logentry .= $logfrag; | 
|---|
| [301] | 3730 | } else { | 
|---|
|  | 3731 | $logentry .= "Added record '$host $type"; | 
|---|
|  | 3732 | $logentry .= " [distance $distance]" if $type eq 'MX'; | 
|---|
|  | 3733 | $logentry .= " [priority $distance] [weight $weight] [port $port]" if $type eq 'SRV'; | 
|---|
|  | 3734 | $logentry .= " $val', TTL $ttl"; | 
|---|
|  | 3735 | } | 
|---|
|  | 3736 | _log($dbh, (group_id => $group, domain_id => $domain_id, rdns_id => $rdns_id, entry => $logentry) ); | 
|---|
|  | 3737 |  | 
|---|
| [37] | 3738 | } # while axfr_next | 
|---|
|  | 3739 |  | 
|---|
| [307] | 3740 | # Detect and handle delegated subzones | 
|---|
|  | 3741 | # Placeholder for when we decide what to actually do with this, see previous comments in NS and CNAME handling. | 
|---|
|  | 3742 | #foreach (keys %suboct) { | 
|---|
|  | 3743 | #  print "found ".($suboct{$_}{ns} ? @{$suboct{$_}{ns}} : '0')." NS records and ". | 
|---|
|  | 3744 | #       ($suboct{$_}{cname} ? @{$suboct{$_}{cname}} : '0')." CNAMEs for $_\n"; | 
|---|
|  | 3745 | #} | 
|---|
|  | 3746 |  | 
|---|
| [37] | 3747 | # Overwrite SOA record | 
|---|
|  | 3748 | if ($rwsoa) { | 
|---|
|  | 3749 | $soaflag = 1; | 
|---|
|  | 3750 | my $sthgetsoa = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?"); | 
|---|
|  | 3751 | my $sthputsoa = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)"); | 
|---|
|  | 3752 | $sthgetsoa->execute($group,$reverse_typemap{SOA}); | 
|---|
|  | 3753 | while (my ($host,$val,$ttl) = $sthgetsoa->fetchrow_array()) { | 
|---|
| [301] | 3754 | $host =~ s/DOMAIN/$zone/g; | 
|---|
|  | 3755 | $val =~ s/DOMAIN/$zone/g; | 
|---|
|  | 3756 | $sthputsoa->execute($zone_id,$host,$reverse_typemap{SOA},$val,$ttl); | 
|---|
| [34] | 3757 | } | 
|---|
| [37] | 3758 | } | 
|---|
| [34] | 3759 |  | 
|---|
| [37] | 3760 | # Overwrite NS records | 
|---|
|  | 3761 | if ($rwns) { | 
|---|
|  | 3762 | $nsflag = 1; | 
|---|
|  | 3763 | my $sthgetns = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?"); | 
|---|
|  | 3764 | my $sthputns = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)"); | 
|---|
|  | 3765 | $sthgetns->execute($group,$reverse_typemap{NS}); | 
|---|
|  | 3766 | while (my ($host,$val,$ttl) = $sthgetns->fetchrow_array()) { | 
|---|
| [301] | 3767 | $host =~ s/DOMAIN/$zone/g; | 
|---|
|  | 3768 | $val =~ s/DOMAIN/$zone/g; | 
|---|
|  | 3769 | $sthputns->execute($zone_id,$host,$reverse_typemap{NS},$val,$ttl); | 
|---|
| [37] | 3770 | } | 
|---|
|  | 3771 | } | 
|---|
| [34] | 3772 |  | 
|---|
| [35] | 3773 | die "No records found;  either $ifrom is not authoritative or doesn't allow transfers\n" if !$nrecs; | 
|---|
|  | 3774 | die "Bad zone:  No SOA record!\n" if !$soaflag; | 
|---|
|  | 3775 | die "Bad zone:  No NS records!\n" if !$nsflag; | 
|---|
|  | 3776 |  | 
|---|
| [37] | 3777 | $dbh->commit; | 
|---|
| [35] | 3778 |  | 
|---|
| [33] | 3779 | }; | 
|---|
|  | 3780 |  | 
|---|
|  | 3781 | if ($@) { | 
|---|
|  | 3782 | my $msg = $@; | 
|---|
|  | 3783 | eval { $dbh->rollback; }; | 
|---|
| [34] | 3784 | return ('FAIL',$msg." $warnmsg"); | 
|---|
| [33] | 3785 | } else { | 
|---|
| [35] | 3786 | return ('WARN', $warnmsg) if $warnmsg; | 
|---|
| [91] | 3787 | return ('OK',"Imported OK"); | 
|---|
| [33] | 3788 | } | 
|---|
|  | 3789 |  | 
|---|
| [37] | 3790 | # it should be impossible to get here. | 
|---|
| [34] | 3791 | return ('WARN',"OOOK!"); | 
|---|
| [33] | 3792 | } # end importAXFR() | 
|---|
|  | 3793 |  | 
|---|
|  | 3794 |  | 
|---|
| [302] | 3795 | ## DNSDB::importBIND() | 
|---|
|  | 3796 | sub importBIND { | 
|---|
|  | 3797 | } # end importBIND() | 
|---|
|  | 3798 |  | 
|---|
|  | 3799 |  | 
|---|
|  | 3800 | ## DNSDB::import_tinydns() | 
|---|
|  | 3801 | sub import_tinydns { | 
|---|
|  | 3802 | } # end import_tinydns() | 
|---|
|  | 3803 |  | 
|---|
|  | 3804 |  | 
|---|
| [103] | 3805 | ## DNSDB::export() | 
|---|
|  | 3806 | # Export the DNS database, or a part of it | 
|---|
|  | 3807 | # Takes database handle, export type, optional arguments depending on type | 
|---|
|  | 3808 | # Writes zone data to targets as appropriate for type | 
|---|
|  | 3809 | sub export { | 
|---|
|  | 3810 | my $dbh = shift; | 
|---|
|  | 3811 | my $target = shift; | 
|---|
|  | 3812 |  | 
|---|
|  | 3813 | if ($target eq 'tiny') { | 
|---|
|  | 3814 | __export_tiny($dbh,@_); | 
|---|
|  | 3815 | } | 
|---|
|  | 3816 | # elsif ($target eq 'foo') { | 
|---|
|  | 3817 | #   __export_foo($dbh,@_); | 
|---|
|  | 3818 | #} | 
|---|
|  | 3819 | # etc | 
|---|
|  | 3820 |  | 
|---|
|  | 3821 | } # end export() | 
|---|
|  | 3822 |  | 
|---|
|  | 3823 |  | 
|---|
|  | 3824 | ## DNSDB::__export_tiny | 
|---|
|  | 3825 | # Internal sub to implement tinyDNS (compatible) export | 
|---|
|  | 3826 | # Takes database handle, filehandle to write export to, optional argument(s) | 
|---|
|  | 3827 | # to determine which data gets exported | 
|---|
|  | 3828 | sub __export_tiny { | 
|---|
|  | 3829 | my $dbh = shift; | 
|---|
|  | 3830 | my $datafile = shift; | 
|---|
|  | 3831 |  | 
|---|
|  | 3832 | ##fixme: slurp up further options to specify particular zone(s) to export | 
|---|
|  | 3833 |  | 
|---|
|  | 3834 | ## Convert a bare number into an octal-coded pair of octets. | 
|---|
|  | 3835 | # Take optional arg to indicate a decimal or hex input.  Defaults to hex. | 
|---|
|  | 3836 | sub octalize { | 
|---|
|  | 3837 | my $tmp = shift; | 
|---|
|  | 3838 | my $srctype = shift || 'h'; # default assumes hex string | 
|---|
|  | 3839 | $tmp = sprintf "%0.4x", hex($tmp) if $srctype eq 'h';       # 0-pad hex to 4 digits | 
|---|
|  | 3840 | $tmp = sprintf "%0.4x", $tmp if $srctype eq 'd';    # 0-pad decimal to 4 hex digits | 
|---|
|  | 3841 | my @o = ($tmp =~ /^(..)(..)$/);     # split into octets | 
|---|
|  | 3842 | return sprintf "\\%0.3o\\%0.3o", hex($o[0]), hex($o[1]);; | 
|---|
|  | 3843 | } | 
|---|
|  | 3844 |  | 
|---|
|  | 3845 | ##fixme: fail if $datafile isn't an open, writable file | 
|---|
|  | 3846 |  | 
|---|
|  | 3847 | # easy case - export all evarything | 
|---|
|  | 3848 | # not-so-easy case - export item(s) specified | 
|---|
|  | 3849 | # todo:  figure out what kind of list we use to export items | 
|---|
|  | 3850 |  | 
|---|
|  | 3851 | my $domsth = $dbh->prepare("SELECT domain_id,domain,status FROM domains WHERE status=1"); | 
|---|
| [130] | 3852 | my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl ". | 
|---|
|  | 3853 | "FROM records WHERE domain_id=?"); | 
|---|
| [103] | 3854 | $domsth->execute(); | 
|---|
|  | 3855 | while (my ($domid,$dom,$domstat) = $domsth->fetchrow_array) { | 
|---|
|  | 3856 | $recsth->execute($domid); | 
|---|
| [130] | 3857 | while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $recsth->fetchrow_array) { | 
|---|
| [108] | 3858 | ##fixme:  need to store location in the db, and retrieve it here. | 
|---|
|  | 3859 | # temporarily hardcoded to empty so we can include it further down. | 
|---|
|  | 3860 | my $loc = ''; | 
|---|
|  | 3861 |  | 
|---|
|  | 3862 | ##fixme:  record validity timestamp. tinydns supports fiddling with timestamps. | 
|---|
|  | 3863 | # note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps. | 
|---|
|  | 3864 | # timestamps are TAI64 | 
|---|
|  | 3865 | # ~~ 2^62 + time() | 
|---|
|  | 3866 | my $stamp = ''; | 
|---|
|  | 3867 |  | 
|---|
| [103] | 3868 | # raw packet in unknown format:  first byte indicates length | 
|---|
|  | 3869 | # of remaining data, allows up to 255 raw bytes | 
|---|
|  | 3870 |  | 
|---|
|  | 3871 | ##fixme?  append . to all host/val hostnames | 
|---|
|  | 3872 | if ($typemap{$type} eq 'SOA') { | 
|---|
|  | 3873 |  | 
|---|
|  | 3874 | # host contains pri-ns:responsible | 
|---|
|  | 3875 | # val is abused to contain refresh:retry:expire:minttl | 
|---|
|  | 3876 | ##fixme:  "manual" serial vs tinydns-autoserial | 
|---|
| [202] | 3877 | # let's be explicit about abusing $host and $val | 
|---|
|  | 3878 | my ($email, $primary) = (split /:/, $host)[0,1]; | 
|---|
|  | 3879 | my ($refresh, $retry, $expire, $min_ttl) = (split /:/, $val)[0,1,2,3]; | 
|---|
|  | 3880 | print $datafile "Z$dom:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3881 |  | 
|---|
|  | 3882 | } elsif ($typemap{$type} eq 'A') { | 
|---|
|  | 3883 |  | 
|---|
| [108] | 3884 | print $datafile "+$host:$val:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3885 |  | 
|---|
|  | 3886 | } elsif ($typemap{$type} eq 'NS') { | 
|---|
|  | 3887 |  | 
|---|
| [108] | 3888 | print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3889 |  | 
|---|
|  | 3890 | } elsif ($typemap{$type} eq 'AAAA') { | 
|---|
|  | 3891 |  | 
|---|
|  | 3892 | print $datafile ":$host:28:"; | 
|---|
|  | 3893 | my $altgrp = 0; | 
|---|
|  | 3894 | my @altconv; | 
|---|
| [108] | 3895 | # Split in to up to 8 groups of hex digits (allows for IPv6 :: 0-collapsing) | 
|---|
| [103] | 3896 | foreach (split /:/, $val) { | 
|---|
|  | 3897 | if (/^$/) { | 
|---|
|  | 3898 | # flag blank entry;  this is a series of 0's of (currently) unknown length | 
|---|
|  | 3899 | $altconv[$altgrp++] = 's'; | 
|---|
|  | 3900 | } else { | 
|---|
|  | 3901 | # call sub to convert 1-4 hex digits to 2 string-rep octal bytes | 
|---|
|  | 3902 | $altconv[$altgrp++] = octalize($_) | 
|---|
|  | 3903 | } | 
|---|
|  | 3904 | } | 
|---|
|  | 3905 | foreach my $octet (@altconv) { | 
|---|
|  | 3906 | # if not 's', output | 
|---|
|  | 3907 | print $datafile $octet unless $octet =~ /^s$/; | 
|---|
|  | 3908 | # if 's', output (9-array length)x literal '\000\000' | 
|---|
|  | 3909 | print $datafile '\000\000'x(9-$altgrp) if $octet =~ /^s$/; | 
|---|
|  | 3910 | } | 
|---|
| [108] | 3911 | print $datafile ":$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3912 |  | 
|---|
|  | 3913 | } elsif ($typemap{$type} eq 'MX') { | 
|---|
|  | 3914 |  | 
|---|
| [108] | 3915 | print $datafile "\@$host"."::$val:$dist:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3916 |  | 
|---|
|  | 3917 | } elsif ($typemap{$type} eq 'TXT') { | 
|---|
|  | 3918 |  | 
|---|
|  | 3919 | ##fixme:  split v-e-r-y long TXT strings?  will need to do so for BIND export, at least | 
|---|
|  | 3920 | $val =~ s/:/\\072/g;    # may need to replace other symbols | 
|---|
| [108] | 3921 | print $datafile "'$host:$val:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3922 |  | 
|---|
|  | 3923 | # by-hand TXT | 
|---|
|  | 3924 | #:deepnet.cx:16:2v\075spf1\040a\040a\072bacon.deepnet.cx\040a\072home.deepnet.cx\040-all:3600 | 
|---|
|  | 3925 | #@       IN      TXT     "v=spf1 a a:bacon.deepnet.cx a:home.deepnet.cx -all" | 
|---|
|  | 3926 | #'deepnet.cx:v=spf1 a a\072bacon.deepnet.cx a\072home.deepnet.cx -all:3600 | 
|---|
|  | 3927 |  | 
|---|
|  | 3928 | #txttest IN      TXT     "v=foo bar:bob kn;ob' \" !@#$%^&*()-=_+[]{}<>?" | 
|---|
|  | 3929 | #:txttest.deepnet.cx:16:\054v\075foo\040bar\072bob\040kn\073ob\047\040\042\040\041\100\043\044\045\136\046\052\050\051-\075\137\053\133\135\173\175\074\076\077:3600 | 
|---|
|  | 3930 |  | 
|---|
|  | 3931 | # very long TXT record as brought in by axfr-get | 
|---|
|  | 3932 | # note tinydns does not support >512-byte RR data, need axfr-dns (for TCP support) for that | 
|---|
|  | 3933 | # also note, tinydns does not seem to support <512, >256-byte RRdata from axfr-get either.  :/ | 
|---|
|  | 3934 | #:longtxt.deepnet.cx:16: | 
|---|
|  | 3935 | #\170this is a very long txt record.  it is really long.  long. very long.  really very long. this is a very long txt record. | 
|---|
|  | 3936 | #\263  it is really long.  long. very long.  really very long. this is a very long txt record.  it is really long.  long. very long.  really very long. this is a very long txt record. | 
|---|
|  | 3937 | #\351 it is really long.  long. very long.  really very long.this is a very long txt record.  it is really long.  long. very long.  really very long. this is a very long txt record.  it is really long.  long. very long.  really very long. | 
|---|
|  | 3938 | #:3600 | 
|---|
|  | 3939 |  | 
|---|
|  | 3940 | } elsif ($typemap{$type} eq 'CNAME') { | 
|---|
|  | 3941 |  | 
|---|
| [108] | 3942 | print $datafile "C$host:$val:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3943 |  | 
|---|
|  | 3944 | } elsif ($typemap{$type} eq 'SRV') { | 
|---|
|  | 3945 |  | 
|---|
|  | 3946 | # data is two-byte values for priority, weight, port, in that order, | 
|---|
|  | 3947 | # followed by length/string data | 
|---|
|  | 3948 |  | 
|---|
|  | 3949 | print $datafile ":$host:33:".octalize($dist,'d').octalize($weight,'d').octalize($port,'d'); | 
|---|
|  | 3950 |  | 
|---|
|  | 3951 | $val .= '.' if $val !~ /\.$/; | 
|---|
|  | 3952 | foreach (split /\./, $val) { | 
|---|
|  | 3953 | printf $datafile "\\%0.3o%s", length($_), $_; | 
|---|
|  | 3954 | } | 
|---|
| [108] | 3955 | print $datafile "\\000:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3956 |  | 
|---|
|  | 3957 | } elsif ($typemap{$type} eq 'RP') { | 
|---|
|  | 3958 |  | 
|---|
|  | 3959 | # RP consists of two mostly free-form strings. | 
|---|
|  | 3960 | # The first is supposed to be an email address with @ replaced by . (as with the SOA contact) | 
|---|
|  | 3961 | # The second is the "hostname" of a TXT record with more info. | 
|---|
|  | 3962 | print $datafile ":$host:17:"; | 
|---|
|  | 3963 | my ($who,$what) = split /\s/, $val; | 
|---|
|  | 3964 | foreach (split /\./, $who) { | 
|---|
|  | 3965 | printf $datafile "\\%0.3o%s", length($_), $_; | 
|---|
|  | 3966 | } | 
|---|
|  | 3967 | print $datafile '\000'; | 
|---|
|  | 3968 | foreach (split /\./, $what) { | 
|---|
|  | 3969 | printf $datafile "\\%0.3o%s", length($_), $_; | 
|---|
|  | 3970 | } | 
|---|
| [108] | 3971 | print $datafile "\\000:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3972 |  | 
|---|
|  | 3973 | } elsif ($typemap{$type} eq 'PTR') { | 
|---|
|  | 3974 |  | 
|---|
|  | 3975 | # must handle both IPv4 and IPv6 | 
|---|
|  | 3976 | ##work | 
|---|
| [108] | 3977 | # data should already be in suitable reverse order. | 
|---|
|  | 3978 | print $datafile "^$host:$val:$ttl:$stamp:$loc\n"; | 
|---|
| [103] | 3979 |  | 
|---|
| [108] | 3980 | } else { | 
|---|
|  | 3981 | # raw record.  we don't know what's in here, so we ASS-U-ME the user has | 
|---|
|  | 3982 | # put it in correctly, since either the user is messing directly with the | 
|---|
|  | 3983 | # database, or the record was imported via AXFR | 
|---|
|  | 3984 | # <split by char> | 
|---|
|  | 3985 | # convert anything not a-zA-Z0-9.- to octal coding | 
|---|
|  | 3986 |  | 
|---|
|  | 3987 | ##fixme: add flag to export "unknown" record types - note we'll probably end up | 
|---|
|  | 3988 | # mangling them since they were written to the DB from Net::DNS::RR::<type>->rdatastr. | 
|---|
|  | 3989 | #print $datafile ":$host:$type:$val:$ttl:$stamp:$loc\n"; | 
|---|
|  | 3990 |  | 
|---|
| [103] | 3991 | } # record type if-else | 
|---|
|  | 3992 |  | 
|---|
|  | 3993 | } # while ($recsth) | 
|---|
|  | 3994 | } # while ($domsth) | 
|---|
|  | 3995 | } # end __export_tiny() | 
|---|
|  | 3996 |  | 
|---|
|  | 3997 |  | 
|---|
| [197] | 3998 | ## DNSDB::mailNotify() | 
|---|
| [283] | 3999 | # Sends notification mail to recipients regarding a DNSDB operation | 
|---|
| [197] | 4000 | sub mailNotify { | 
|---|
|  | 4001 | my $dbh = shift; | 
|---|
|  | 4002 | my ($subj,$message) = @_; | 
|---|
|  | 4003 |  | 
|---|
|  | 4004 | return if $config{mailhost} eq 'smtp.example.com';   # do nothing if still using default SMTP host. | 
|---|
|  | 4005 |  | 
|---|
|  | 4006 | my $mailer = Net::SMTP->new($config{mailhost}, Hello => "dnsadmin.$config{domain}"); | 
|---|
|  | 4007 |  | 
|---|
|  | 4008 | my $mailsender = ($config{mailsender} ? $config{mailsender} : $config{mailnotify}); | 
|---|
|  | 4009 |  | 
|---|
|  | 4010 | $mailer->mail($mailsender); | 
|---|
|  | 4011 | $mailer->to($config{mailnotify}); | 
|---|
| [198] | 4012 | $mailer->data("From: \"$config{mailname}\" <$mailsender>\n", | 
|---|
|  | 4013 | "To: <$config{mailnotify}>\n", | 
|---|
| [197] | 4014 | "Date: ".strftime("%a, %d %b %Y %H:%M:%S %z",localtime)."\n", | 
|---|
|  | 4015 | "Subject: $subj\n", | 
|---|
|  | 4016 | "X-Mailer: DNSAdmin Notify v".sprintf("%.1d",$DNSDB::VERSION)."\n", | 
|---|
|  | 4017 | "Organization: $config{orgname}\n", | 
|---|
|  | 4018 | "\n$message\n"); | 
|---|
|  | 4019 | $mailer->quit; | 
|---|
|  | 4020 | } | 
|---|
|  | 4021 |  | 
|---|
| [2] | 4022 | # shut Perl up | 
|---|
|  | 4023 | 1; | 
|---|