Changeset 544 for branches/stable/DNSDB.pm
- Timestamp:
- 12/10/13 17:15:56 (10 years ago)
- Location:
- branches/stable
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
branches/stable
-
branches/stable/DNSDB.pm
r438 r544 28 28 use Crypt::PasswdMD5; 29 29 use Net::SMTP; 30 use NetAddr::IP ;30 use NetAddr::IP qw(:lower); 31 31 use POSIX; 32 32 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); … … 39 39 &changeGroup 40 40 &loadConfig &connectDB &finish 41 &addDomain &delDomain &domainName &domainID 41 &addDomain &delDomain &domainName &revName &domainID &addRDNS 42 &getZoneCount &getZoneList 42 43 &addGroup &delGroup &getChildren &groupName 43 44 &addUser &updateUser &delUser &userFullName &userStatus &getUserData 44 45 &getSOA &getRecLine &getDomRecs &getRecCount 45 46 &addRec &updateRec &delRec 46 &getParents 47 &getTypelist 48 &parentID 47 49 &isParent 48 50 &domStatus &importAXFR … … 59 61 &changeGroup 60 62 &loadConfig &connectDB &finish 61 &addDomain &delDomain &domainName &domainID 63 &addDomain &delDomain &domainName &revName &domainID &addRDNS 64 &getZoneCount &getZoneList 62 65 &addGroup &delGroup &getChildren &groupName 63 66 &addUser &updateUser &delUser &userFullName &userStatus &getUserData 64 67 &getSOA &getRecLine &getDomRecs &getRecCount 65 68 &addRec &updateRec &delRec 66 &getParents 69 &getTypelist 70 &parentID 67 71 &isParent 68 72 &domStatus &importAXFR … … 139 143 perpage => 15, 140 144 ); 145 146 ## (Semi)private variables 147 # Hash of functions for validating record types. Filled in initGlobals() since 148 # it relies on visibility flags from the rectypes table in the DB 149 my %validators; 150 151 152 ## 153 ## utility functions 154 # _rectable() 155 # Takes default+rdns flags, returns appropriate table name 156 sub _rectable { 157 my $def = shift; 158 my $rev = shift; 159 160 return 'records' if $def ne 'y'; 161 return 'default_records' if $rev ne 'y'; 162 return 'default_rev_records'; 163 } # end _rectable() 164 165 # _recparent() 166 # Takes default+rdns flags, returns appropriate parent-id column name 167 sub _recparent { 168 my $def = shift; 169 my $rev = shift; 170 171 return 'group_id' if $def eq 'y'; 172 return 'rdns_id' if $rev eq 'y'; 173 return 'domain_id'; 174 } # end _recparent() 175 176 # Check an IP to be added in a reverse zone to see if it's really in the requested parent. 177 # Takes a database handle, default and reverse flags, IP (fragment) to check, parent zone ID, 178 # and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for 179 # database insertion) 180 sub _ipparent { 181 my $dbh = shift; 182 my $defrec = shift; 183 my $revrec = shift; 184 my $val = shift; 185 my $id = shift; 186 my $addr = shift; 187 188 return if $revrec ne 'y'; # this sub not useful in forward zones 189 190 $$addr = NetAddr::IP->new($$val); #necessary? 191 192 # subsub to split, reverse, and overlay an IP fragment on a netblock 193 sub __rev_overlay { 194 my $splitme = shift; # ':' or '.', m'lud? 195 my $parnet = shift; 196 my $val = shift; 197 my $addr = shift; 198 199 my $joinme = $splitme; 200 $splitme = '\.' if $splitme eq '.'; 201 my @working = reverse(split($splitme, $parnet->addr)); 202 my @parts = reverse(split($splitme, $$val)); 203 for (my $i = 0; $i <= $#parts; $i++) { 204 $working[$i] = $parts[$i]; 205 } 206 my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0; 207 return 0 unless $checkme->within($parnet); 208 $$addr = $checkme; # force "correct" IP to be recorded. 209 return 1; 210 } 211 212 my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id)); 213 my $parnet = NetAddr::IP->new($parstr); 214 215 # Fail early on v6-in-v4 or v4-in-v6. We're not accepting these ATM. 216 return 0 if $parnet->addr =~ /\./ && $$val =~ /:/; 217 return 0 if $parnet->addr =~ /:/ && $$val =~ /\./; 218 219 if ($$addr && $$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/) { 220 # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address. 221 # the rest we have to restructure before fiddling. *sigh* 222 return 1 if $$addr->within($parnet); 223 } else { 224 # We don't have a complete IP in $$val (yet) 225 if ($parnet->addr =~ /:/) { 226 $$val =~ s/^:+//; # gotta strip'em all... 227 return __rev_overlay(':', $parnet, $val, $addr); 228 } 229 if ($parnet->addr =~ /\./) { 230 $$val =~ s/^\.+//; 231 return __rev_overlay('.', $parnet, $val, $addr); 232 } 233 # should be impossible to get here... 234 } 235 # ... and here. 236 # can't do nuttin' in forward zones 237 } # end _ipparent() 238 239 # A little different than _ipparent above; this tries to *find* the parent zone of a hostname 240 sub _hostparent { 241 my $dbh = shift; 242 my $hname = shift; 243 244 my @hostbits = split /\./, $hname; 245 my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE domain = ? GROUP BY domain_id"); 246 foreach (@hostbits) { 247 $sth->execute($hname); 248 my ($found, $parid) = $sth->fetchrow_array; 249 if ($found) { 250 return $parid; 251 } 252 $hname =~ s/^$_\.//; 253 } 254 } # end _hostparent() 255 256 ## 257 ## Record validation subs. 258 ## 259 260 # A record 261 sub _validate_1 { 262 my $dbh = shift; 263 264 my %args = @_; 265 266 return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y'; 267 268 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 269 # or the intended parent domain for live records. 270 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 271 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 272 273 # Check IP is well-formed, and that it's a v4 address 274 # Fail on "compact" IPv4 variants, because they are not consistent and predictable. 275 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") 276 unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/; 277 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") 278 unless $args{addr} && !$args{addr}->{isv6}; 279 # coerce IP/value to normalized form for storage 280 ${$args{val}} = $args{addr}->addr; 281 282 return ('OK','OK'); 283 } # done A record 284 285 # NS record 286 sub _validate_2 { 287 my $dbh = shift; 288 289 my %args = @_; 290 291 # Coerce the hostname to "DOMAIN" for forward default records, "ZONE" for reverse default records, 292 # or the intended parent zone for live records. 293 ##fixme: allow for delegating <subdomain>.DOMAIN? 294 if ($args{revrec} eq 'y') { 295 my $pname = ($args{defrec} eq 'y' ? 'ZONE' : revName($dbh,$args{id})); 296 ${$args{host}} = $pname if ${$args{host}} ne $pname; 297 } else { 298 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 299 ${$args{host}} = $pname if ${$args{host}} ne $pname; 300 } 301 302 # Let this lie for now. Needs more magic. 303 # # Check IP is well-formed, and that it's a v4 address 304 # return ('FAIL',"A record must be a valid IPv4 address") 305 # unless $addr && !$addr->{isv6}; 306 # # coerce IP/value to normalized form for storage 307 # $$val = $addr->addr; 308 309 return ('OK','OK'); 310 } # done NS record 311 312 # CNAME record 313 sub _validate_5 { 314 my $dbh = shift; 315 316 my %args = @_; 317 318 # Not really true, but these are only useful for delegating smaller-than-/24 IP blocks. 319 # This is fundamentally a messy operation and should really just be taken care of by the 320 # export process, not manual maintenance of the necessary records. 321 return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y'; 322 323 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 324 # or the intended parent domain for live records. 325 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 326 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 327 328 return ('OK','OK'); 329 } # done CNAME record 330 331 # SOA record 332 sub _validate_6 { 333 # Smart monkeys won't stick their fingers in here; we have 334 # separate dedicated routines to deal with SOA records. 335 return ('OK','OK'); 336 } # done SOA record 337 338 # PTR record 339 sub _validate_12 { 340 my $dbh = shift; 341 342 my %args = @_; 343 344 if ($args{revrec} eq 'y') { 345 if ($args{defrec} eq 'n') { 346 return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id})) 347 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr}); 348 ${$args{val}} = $args{addr}->addr; 349 } else { 350 if (${$args{val}} =~ /\./) { 351 # looks like a v4 or fragment 352 if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) { 353 # woo! a complete IP! validate it and normalize, or fail. 354 $args{addr} = NetAddr::IP->new(${$args{val}}) 355 or return ('FAIL', "IP/value looks like IPv4 but isn't valid"); 356 ${$args{val}} = $args{addr}->addr; 357 } else { 358 ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/; 359 } 360 } elsif (${$args{val}} =~ /[a-f:]/) { 361 # looks like a v6 or fragment 362 ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/; 363 if ($args{addr}) { 364 if ($args{addr}->addr =~ /^0/) { 365 ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/; 366 } else { 367 ${$args{val}} = $args{addr}->addr; 368 } 369 } 370 } else { 371 # bare number (probably). These could be v4 or v6, so we'll 372 # expand on these on creation of a reverse zone. 373 ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/; 374 } 375 ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/; 376 } 377 378 # Multiple PTR records do NOT generally do what most people believe they do, 379 # and tend to fail in the most awkward way possible. Check and warn. 380 # We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12" 381 382 my @checkvals = (${$args{val}}); 383 if (${$args{val}} =~ /,/) { 384 # push . and :: variants into checkvals if val has , 385 my $tmp; 386 ($tmp = ${$args{val}}) =~ s/,/./; 387 push @checkvals, $tmp; 388 ($tmp = ${$args{val}}) =~ s/,/::/; 389 push @checkvals, $tmp; 390 } 391 my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?"); 392 foreach my $checkme (@checkvals) { 393 my $ptrcount; 394 ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 395 " WHERE val = ?", undef, ($checkme)); 396 return ('WARN', "PTR record for $checkme already exists; adding another will probably not do what you want") 397 if $ptrcount; 398 } 399 } else { 400 # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations 401 # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct 402 # PTR records on export 403 return ('FAIL',"Forward zones cannot contain PTR records"); 404 } 405 406 return ('OK','OK'); 407 } # done PTR record 408 409 # MX record 410 sub _validate_15 { 411 my $dbh = shift; 412 413 my %args = @_; 414 415 # Not absolutely true but WTF use is an MX record for a reverse zone? 416 return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y'; 417 418 return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}}); 419 ${$args{dist}} =~ s/\s*//g; 420 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; 421 422 ${$args{fields}} = "distance,"; 423 push @{$args{vallist}}, ${$args{dist}}; 424 425 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 426 # or the intended parent domain for live records. 427 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 428 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 429 430 return ('OK','OK'); 431 } # done MX record 432 433 # TXT record 434 sub _validate_16 { 435 # Could arguably put a WARN return here on very long (>512) records 436 return ('OK','OK'); 437 } # done TXT record 438 439 # RP record 440 sub _validate_17 { 441 # Probably have to validate these some day 442 return ('OK','OK'); 443 } # done RP record 444 445 # AAAA record 446 sub _validate_28 { 447 my $dbh = shift; 448 449 my %args = @_; 450 451 return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y'; 452 453 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 454 # or the intended parent domain for live records. 455 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 456 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 457 458 # Check IP is well-formed, and that it's a v6 address 459 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address") 460 unless $args{addr} && $args{addr}->{isv6}; 461 # coerce IP/value to normalized form for storage 462 ${$args{val}} = $args{addr}->addr; 463 464 return ('OK','OK'); 465 } # done AAAA record 466 467 # SRV record 468 sub _validate_33 { 469 my $dbh = shift; 470 471 my %args = @_; 472 473 # Not absolutely true but WTF use is an SRV record for a reverse zone? 474 return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y'; 475 476 return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}}); 477 ${$args{dist}} =~ s/\s*//g; 478 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; 479 480 return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]") 481 unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/; 482 return ('FAIL',"Port and weight are required for SRV records") 483 unless defined(${$args{weight}}) && defined(${$args{port}}); 484 ${$args{weight}} =~ s/\s*//g; 485 ${$args{port}} =~ s/\s*//g; 486 487 return ('FAIL',"Port and weight are required, and must be numeric") 488 unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/; 489 490 ${$args{fields}} = "distance,weight,port,"; 491 push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}}); 492 493 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 494 # or the intended parent domain for live records. 495 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 496 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 497 498 return ('OK','OK'); 499 } # done SRV record 500 501 # Now the custom types 502 503 # A+PTR record. With a very little bit of magic we can also use this sub to validate AAAA+PTR. Whee! 504 sub _validate_65280 { 505 my $dbh = shift; 506 507 my %args = @_; 508 509 my $code = 'OK'; 510 my $msg = 'OK'; 511 512 if ($args{defrec} eq 'n') { 513 # live record; revrec determines whether we validate the PTR or A component first. 514 515 if ($args{revrec} eq 'y') { 516 ($code,$msg) = _validate_12($dbh, %args); 517 return ($code,$msg) if $code eq 'FAIL'; 518 519 # Check if the reqested domain exists. If not, coerce the type down to PTR and warn. 520 if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) { 521 my $addmsg = "Record added as PTR instead of $typemap{${$args{rectype}}}; domain not found for ${$args{host}}"; 522 $msg .= "\n$addmsg" if $code eq 'WARN'; 523 $msg = $addmsg if $code eq 'OK'; 524 ${$args{rectype}} = $reverse_typemap{PTR}; 525 return ('WARN', $msg); 526 } 527 528 # Add domain ID to field list and values 529 ${$args{fields}} .= "domain_id,"; 530 push @{$args{vallist}}, ${$args{domid}}; 531 532 } else { 533 ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280; 534 ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281; 535 return ($code,$msg) if $code eq 'FAIL'; 536 537 # Check if the requested reverse zone exists - note, an IP fragment won't 538 # work here since we don't *know* which parent to put it in. 539 # ${$args{val}} has been validated as a valid IP by now, in one of the above calls. 540 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?". 541 " ORDER BY masklen(revnet) DESC", undef, (${$args{val}})); 542 if (!$revid) { 543 $msg = "Record added as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA'). 544 " instead of $typemap{${$args{rectype}}}; reverse zone not found for ${$args{val}}"; 545 ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA}); 546 return ('WARN', $msg); 547 } 548 549 # Check for duplicate PTRs. Note we don't have to play games with $code and $msg, because 550 # by definition there can't be duplicate PTRs if the reverse zone isn't managed here. 551 my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 552 " WHERE val = ?", undef, ${$args{val}}); 553 if ($ptrcount) { 554 $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want"; 555 $code = 'WARN'; 556 } 557 558 ${$args{fields}} .= "rdns_id,"; 559 push @{$args{vallist}}, $revid; 560 } 561 562 } else { # defrec eq 'y' 563 if ($args{revrec} eq 'y') { 564 ($code,$msg) = _validate_12($dbh, %args); 565 return ($code,$msg) if $code eq 'FAIL'; 566 if (${$args{rectype}} == 65280) { 567 return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment") 568 if ${$args{val}} =~ /:/; 569 ${$args{val}} =~ s/^ZONE,/ZONE./; # Clean up after uncertain IP-fragment-type from _validate_12 570 } elsif (${$args{rectype}} == 65281) { 571 return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment") 572 if ${$args{val}} =~ /\./; 573 ${$args{val}} =~ s/^ZONE,/ZONE::/; # Clean up after uncertain IP-fragment-type from _validate_12 574 } 575 } else { 576 # This is easy. I also can't see a real use-case for A/AAAA+PTR in *all* forward 577 # domains, since you wouldn't be able to substitute both domain and reverse zone 578 # sanely, and you'd end up with guaranteed over-replicated PTR records that would 579 # confuse the hell out of pretty much anything that uses them. 580 ##fixme: make this a config flag? 581 return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains"); 582 } 583 } 584 585 return ($code, $msg); 586 } # done A+PTR record 587 588 # AAAA+PTR record 589 # A+PTR above has been magicked to handle AAAA+PTR as well. 590 sub _validate_65281 { 591 return _validate_65280(@_); 592 } # done AAAA+PTR record 593 594 # PTR template record 595 sub _validate_65282 { 596 return ('OK','OK'); 597 } # done PTR template record 598 599 # A+PTR template record 600 sub _validate_65283 { 601 return ('OK','OK'); 602 } # done AAAA+PTR template record 603 604 # AAAA+PTR template record 605 sub _validate_65284 { 606 return ('OK','OK'); 607 } # done AAAA+PTR template record 608 141 609 142 610 … … 327 795 328 796 # load record types from database 329 my $sth = $dbh->prepare(" select val,name fromrectypes");797 my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes"); 330 798 $sth->execute; 331 while (my ($recval,$recname ) = $sth->fetchrow_array()) {799 while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) { 332 800 $typemap{$recval} = $recname; 333 801 $reverse_typemap{$recname} = $recval; 802 # now we fill the record validation function hash 803 if ($stdflag < 5) { 804 my $fn = "_validate_$recval"; 805 $validators{$recval} = \&$fn; 806 } else { 807 my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }"; 808 $validators{$recval} = eval $fn; 809 } 334 810 } 335 811 } # end initGlobals … … 534 1010 # Log an action 535 1011 # Internal sub 536 # Takes a database handle, domain_id, user_id, group_id, email, name and log entry 1012 # Takes a database handle and log entry hash containing at least: 1013 # user_id, group_id, log entry 1014 # and optionally one or more of: 1015 # username/email, user full name, domain_id, rdns_id 537 1016 ##fixme: convert to trailing hash for user info 538 1017 # User info must contain a (user ID OR username)+fullname 539 1018 sub _log { 540 1019 my $dbh = shift; 541 my ($domain_id,$user_id,$group_id,$username,$name,$entry) = @_; 1020 1021 my %args = @_; 1022 1023 $args{rdns_id} = 0 if !$args{rdns_id}; 1024 $args{domain_id} = 0 if !$args{domain_id}; 542 1025 543 1026 ##fixme: need better way(s?) to snag userinfo for log entries. don't want to have 544 1027 # to pass around yet *another* constant (already passing $dbh, shouldn't need to) 545 1028 my $fullname; 546 if (!$user_id) { 547 ($user_id, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users". 548 " WHERE username=?", undef, ($username)); 549 } elsif (!$username) { 550 ($username, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users". 551 " WHERE user_id=?", undef, ($user_id)); 552 } else { 1029 if (!$args{user_id}) { 1030 ($args{user_id}, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users". 1031 " WHERE username=?", undef, ($args{username})); 1032 } 1033 if (!$args{username}) { 1034 ($args{username}, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users". 1035 " WHERE user_id=?", undef, ($args{user_id})); 1036 } 1037 if (!$args{fullname}) { 553 1038 ($fullname) = $dbh->selectrow_array("SELECT firstname || ' ' || lastname FROM users". 554 " WHERE user_id=?", undef, ($ user_id));555 } 556 557 $ name = $fullname if !$name;1039 " WHERE user_id=?", undef, ($args{user_id})); 1040 } 1041 1042 $args{name} = $fullname if !$args{name}; 558 1043 559 1044 ##fixme: farm out the actual logging to different subs for file, syslog, internal, etc based on config 560 $dbh->do("INSERT INTO log (domain_id, user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?)", undef,561 ($domain_id,$user_id,$group_id,$username,$name,$entry));562 # 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 563 # 1 2 3 4 5 6 7 1045 $dbh->do("INSERT INTO log (domain_id,rdns_id,user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?,?)", 1046 undef, 1047 ($args{domain_id},$args{rdns_id},$args{user_id},$args{group_id},$args{username},$args{name},$args{entry})); 1048 564 1049 } # end _log 565 1050 … … 619 1104 ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain)); 620 1105 621 _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname},622 "Added ".($state ? 'active' : 'inactive')." domain $domain");1106 _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{username}, 1107 entry => "Added ".($state ? 'active' : 'inactive')." domain $domain")); 623 1108 624 1109 # ... and now we construct the standard records from the default set. NB: group should be variable. … … 634 1119 my @tmp1 = split /:/, $host; 635 1120 my @tmp2 = split /:/, $val; 636 _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname}, 1121 _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, 1122 username => $userinfo{username}, entry => 637 1123 "[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 638 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl") ;1124 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); 639 1125 } else { 640 1126 my $logentry = "[new $domain] Added record '$host $typemap{$type}"; 641 1127 $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX'; 642 1128 $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV'; 643 _log($dbh, $dom_id, $userinfo{id}, $group, $userinfo{name}, $userinfo{fullname}, 644 $logentry." $val', TTL $ttl"); 1129 _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, 1130 username => $userinfo{username}, entry => 1131 $logentry." $val', TTL $ttl")); 645 1132 } 646 1133 } … … 713 1200 714 1201 1202 ## DNSDB::revName() 1203 # Return the reverse zone name based on an rDNS ID 1204 # Takes a database handle and the rDNS ID 1205 # Returns the reverse zone name or undef on failure 1206 sub revName { 1207 $errstr = ''; 1208 my $dbh = shift; 1209 my $revid = shift; 1210 my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) ); 1211 $errstr = $DBI::errstr if !$revname; 1212 return $revname if $revname; 1213 } # end revName() 1214 1215 715 1216 ## DNSDB::domainID() 716 1217 # Takes a database handle and domain name … … 724 1225 return $domid if $domid; 725 1226 } # end domainID() 1227 1228 1229 ## DNSDB::addRDNS 1230 # Adds a reverse DNS zone 1231 # Takes a database handle, CIDR block, numeric group, boolean(ish) state (active/inactive), 1232 # and user info hash (for logging). 1233 # Returns a status code and message 1234 sub addRDNS { 1235 my $dbh = shift; 1236 my $zone = NetAddr::IP->new(shift); 1237 return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/); 1238 my $revpatt = shift; 1239 my $group = shift; 1240 my $state = shift; 1241 1242 my %userinfo = @_; # remaining bits. 1243 # user ID, username, user full name 1244 1245 $state = 1 if $state =~ /^active$/; 1246 $state = 1 if $state =~ /^on$/; 1247 $state = 0 if $state =~ /^inactive$/; 1248 $state = 0 if $state =~ /^off$/; 1249 1250 return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/; 1251 1252 # quick check to start to see if we've already got one 1253 my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revzone=?", undef, ("$zone")); 1254 1255 return ('FAIL', "Zone already exists") if $rdns_id; 1256 1257 # Allow transactions, and raise an exception on errors so we can catch it later. 1258 # Use local to make sure these get "reset" properly on exiting this block 1259 local $dbh->{AutoCommit} = 0; 1260 local $dbh->{RaiseError} = 1; 1261 1262 #$dbh->selectrow_array("SELECT currval('users_user_id_seq')"); 1263 # Wrap all the SQL in a transaction 1264 eval { 1265 # insert the domain... 1266 $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($zone, $group, $state)); 1267 1268 # get the ID... 1269 ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); 1270 1271 _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{name}, 1272 entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone")); 1273 1274 # ... and now we construct the standard records from the default set. NB: group should be variable. 1275 my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?"); 1276 my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,host,type,val,ttl)". 1277 " VALUES ($rdns_id,?,?,?,?)"); 1278 $sth->execute($group); 1279 while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) { 1280 $host =~ s/ADMINDOMAIN/$config{domain}/g; 1281 ##work 1282 # - replace ZONE in $val 1283 # - skip records not appropriate for the zone (skip A+PTR on v6 zones, and AAAA+PTR on v4 zones) 1284 # $val =~ s/DOMAIN/$domain/g; 1285 $sth_in->execute($host,$type,$val,$ttl); 1286 if ($typemap{$type} eq 'SOA') { 1287 my @tmp1 = split /:/, $host; 1288 my @tmp2 = split /:/, $val; 1289 _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, 1290 username => $userinfo{name}, entry => 1291 "[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 1292 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); 1293 } else { 1294 my $logentry = "[new $zone] Added record '$host $typemap{$type}"; 1295 # $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX'; 1296 # $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV'; 1297 _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, 1298 username => $userinfo{name}, entry => 1299 $logentry." $val', TTL $ttl")); 1300 } 1301 } 1302 1303 # once we get here, we should have suceeded. 1304 $dbh->commit; 1305 }; # end eval 1306 1307 if ($@) { 1308 my $msg = $@; 1309 eval { $dbh->rollback; }; 1310 return ('FAIL',$msg); 1311 } else { 1312 return ('OK',$rdns_id); 1313 } 1314 1315 } # end addRDNS() 1316 1317 1318 ## DNSDB::getZoneCount 1319 # Get count of zones in group or groups 1320 # Takes a database handle and hash containing: 1321 # - the "current" group 1322 # - an array of "acceptable" groups 1323 # - a flag for forward/reverse zones 1324 # - Optionally accept a "starts with" and/or "contains" filter argument 1325 # Returns an integer count of the resulting zone list. 1326 sub getZoneCount { 1327 my $dbh = shift; 1328 1329 my %args = @_; 1330 1331 my @filterargs; 1332 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 1333 push @filterargs, "^$args{startwith}" if $args{startwith}; 1334 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones 1335 push @filterargs, $args{filter} if $args{filter}; 1336 1337 my $sql; 1338 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read 1339 if ($args{revrec} eq 'n') { 1340 $sql = "SELECT count(*) FROM domains". 1341 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1342 ($args{startwith} ? " AND domain ~* ?" : ''). 1343 ($args{filter} ? " AND domain ~* ?" : ''); 1344 } else { 1345 $sql = "SELECT count(*) FROM revzones". 1346 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1347 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). 1348 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); 1349 } 1350 my ($count) = $dbh->selectrow_array($sql, undef, @filterargs); 1351 return $count; 1352 } # end getZoneCount() 1353 1354 1355 ## DNSDB::getZoneList() 1356 # Get a list of zones in the specified group(s) 1357 # Takes the same arguments as getZoneCount() above 1358 # Returns a reference to an array of hashrefs suitable for feeding to HTML::Template 1359 sub getZoneList { 1360 my $dbh = shift; 1361 1362 my %args = @_; 1363 1364 my @zonelist; 1365 1366 $args{sortorder} = 'ASC' if !grep $args{sortorder}, ('ASC','DESC'); 1367 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 1368 1369 my @filterargs; 1370 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 1371 push @filterargs, "^$args{startwith}" if $args{startwith}; 1372 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones 1373 push @filterargs, $args{filter} if $args{filter}; 1374 1375 my $sql; 1376 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read 1377 if ($args{revrec} eq 'n') { 1378 $args{sortby} = 'domain' if !grep $args{sortby}, ('revnet','group','status'); 1379 $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains". 1380 " INNER JOIN groups ON domains.group_id=groups.group_id". 1381 " WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1382 ($args{startwith} ? " AND domain ~* ?" : ''). 1383 ($args{filter} ? " AND domain ~* ?" : ''); 1384 } else { 1385 ##fixme: arguably startwith here is irrelevant. depends on the UI though. 1386 $args{sortby} = 'revnet' if !grep $args{sortby}, ('domain','group','status'); 1387 $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones". 1388 " INNER JOIN groups ON revzones.group_id=groups.group_id". 1389 " WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1390 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). 1391 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); 1392 } 1393 # A common tail. 1394 $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ". 1395 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}". 1396 " OFFSET ".$args{offset}*$config{perpage}); 1397 my $sth = $dbh->prepare($sql); 1398 $sth->execute(@filterargs); 1399 my $rownum = 0; 1400 1401 while (my @data = $sth->fetchrow_array) { 1402 my %row; 1403 $row{domainid} = $data[0]; 1404 $row{domain} = $data[1]; 1405 $row{status} = $data[2]; 1406 $row{group} = $data[3]; 1407 push @zonelist, \%row; 1408 } 1409 1410 return \@zonelist; 1411 } # end getZoneList() 726 1412 727 1413 … … 762 1448 $sth->execute($pargroup,$groupname); 763 1449 764 $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?"); 765 $sth->execute($groupname); 766 my ($groupid) = $sth->fetchrow_array(); 1450 my ($groupid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname)); 767 1451 768 1452 # Permissions … … 789 1473 790 1474 # Default records 791 $sth= $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".1475 my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ". 792 1476 "VALUES ($groupid,?,?,?,?,?,?,?)"); 1477 my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ". 1478 "VALUES ($groupid,?,?,?,?)"); 793 1479 if ($inherit) { 794 1480 # Duplicate records from parent. Actually relying on inherited records feels … … 797 1483 $sth2->execute($pargroup); 798 1484 while (my @clonedata = $sth2->fetchrow_array) { 799 $sth->execute(@clonedata); 1485 $sthf->execute(@clonedata); 1486 } 1487 # And now the reverse records 1488 $sth2 = $dbh->prepare("SELECT group_id,host,type,val,ttl FROM default_rev_records WHERE group_id=?"); 1489 $sth2->execute($pargroup); 1490 while (my @clonedata = $sth2->fetchrow_array) { 1491 $sthr->execute(@clonedata); 800 1492 } 801 1493 } else { … … 803 1495 # reasonable basic defaults for SOA, MX, NS, and minimal hosting 804 1496 # could load from a config file, but somewhere along the line we need hardcoded bits. 805 $sth->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400); 806 $sth->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200); 807 $sth->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200); 808 $sth->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200); 809 $sth->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200); 810 $sth->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200); 1497 $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400); 1498 $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200); 1499 $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200); 1500 $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200); 1501 $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200); 1502 $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200); 1503 # reasonable basic defaults for generic reverse zone. Same as initial SQL tabledef. 1504 $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400); 1505 $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600); 811 1506 } 812 1507 … … 1222 1917 ## DNSDB::getSOA() 1223 1918 # Return all suitable fields from an SOA record in separate elements of a hash 1224 # Takes a database handle, default/live flag, and group (default) or domain (live)ID1919 # Takes a database handle, default/live flag, domain/reverse flag, and parent ID 1225 1920 sub getSOA { 1226 1921 $errstr = ''; 1227 1922 my $dbh = shift; 1228 1923 my $def = shift; 1924 my $rev = shift; 1229 1925 my $id = shift; 1230 1926 my %ret; 1231 1927 1232 # (ab)use distance and weight columns to store SOA data 1233 1234 my $sql = "SELECT record_id,host,val,ttl,distance from"; 1235 if ($def eq 'def' or $def eq 'y') { 1236 $sql .= " default_records WHERE group_id=? AND type=$reverse_typemap{SOA}"; 1237 } else { 1238 # we're editing a live SOA record; find based on domain 1239 $sql .= " records WHERE domain_id=? AND type=$reverse_typemap{SOA}"; 1240 } 1928 # (ab)use distance and weight columns to store SOA data? can't for default_rev_records... 1929 # - should really attach serial to the zone parent somewhere 1930 1931 my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev). 1932 " WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}"; 1933 1241 1934 my $sth = $dbh->prepare($sql); 1242 1935 $sth->execute($id); 1243 1244 my ($recid,$host,$val,$ttl,$serial) = $sth->fetchrow_array() or return; 1936 ##fixme: stick a flag somewhere if the record doesn't exist. by the API, this is an impossible case, but... 1937 1938 my ($recid,$host,$val,$ttl) = $sth->fetchrow_array() or return; 1245 1939 my ($contact,$prins) = split /:/, $host; 1246 1940 my ($refresh,$retry,$expire,$minttl) = split /:/, $val; … … 1248 1942 $ret{recid} = $recid; 1249 1943 $ret{ttl} = $ttl; 1250 $ret{serial} = $serial; 1944 # $ret{serial} = $serial; # ca't use distance for serial with default_rev_records 1251 1945 $ret{prins} = $prins; 1252 1946 $ret{contact} = $contact; … … 1260 1954 1261 1955 1956 ## DNSDB::updateSOA() 1957 # Update the specified SOA record 1958 # Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash 1959 sub updateSOA { 1960 my $dbh = shift; 1961 my $defrec = shift; 1962 my $revrec = shift; 1963 1964 my %soa = @_; 1965 1966 ##fixme: data validation: make sure {recid} is really the SOA for {parent} 1967 my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6"; 1968 $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}", 1969 $soa{ttl}, $soa{recid})); 1970 1971 } # end updateSOA() 1972 1973 1262 1974 ## DNSDB::getRecLine() 1263 1975 # Return all data fields for a zone record in separate elements of a hash 1264 # Takes a database handle, default/live flag, and record ID1976 # Takes a database handle, default/live flag, forward/reverse flag, and record ID 1265 1977 sub getRecLine { 1266 1978 $errstr = ''; 1267 1979 my $dbh = shift; 1268 my $def = shift; 1980 my $defrec = shift; 1981 my $revrec = shift; 1269 1982 my $id = shift; 1270 1983 1271 my $sql = "SELECT record_id,host,type,val, distance,weight,port,ttl".1272 (($def eq 'def' or $def eq 'y') ? ',group_id FROM default_' : ',domain_id FROM ').1273 "recordsWHERE record_id=?";1984 my $sql = "SELECT record_id,host,type,val,ttl".($revrec eq 'n' ? ',distance,weight,port' : ''). 1985 (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM '). 1986 _rectable($defrec,$revrec)." WHERE record_id=?"; 1274 1987 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) ); 1275 1988 … … 1284 1997 } 1285 1998 1286 $ret->{parid} = (($def eq 'def' or $def eq 'y') ? $ret->{group_id} : $ret->{domain_id}); 1999 # explicitly set a parent id 2000 if ($defrec eq 'y') { 2001 $ret->{parid} = $ret->{group_id}; 2002 } else { 2003 $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id}); 2004 # and a secondary if we have a custom type that lives in both a forward and reverse zone 2005 $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279; 2006 } 1287 2007 1288 2008 return $ret; … … 1299 2019 $errstr = ''; 1300 2020 my $dbh = shift; 1301 my $type = shift; 2021 my $def = shift; 2022 my $rev = shift; 1302 2023 my $id = shift; 1303 2024 my $nrecs = shift || 'all'; … … 1310 2031 my $filter = shift || ''; 1311 2032 1312 $type = 'y' if $type eq 'def'; 1313 1314 my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.distance,r.weight,r.port,r.ttl FROM "; 1315 $sql .= "default_" if $type eq 'y'; 1316 $sql .= "records r "; 2033 my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl"; 2034 $sql .= ",r.distance,r.weight,r.port" if $rev eq 'n'; 2035 $sql .= " FROM "._rectable($def,$rev)." r "; 1317 2036 1318 2037 # whee! multisort means just passing comma-separated fields in sortby! … … 1326 2045 1327 2046 $sql .= "INNER JOIN rectypes t ON r.type=t.val "; # for sorting by type alphabetically 1328 if ($type eq 'y') { 1329 $sql .= "WHERE r.group_id=?"; 1330 } else { 1331 $sql .= "WHERE r.domain_id=?"; 1332 } 2047 $sql .= "WHERE "._recparent($def,$rev)." = ?"; 1333 2048 $sql .= " AND NOT r.type=$reverse_typemap{SOA}"; 1334 2049 $sql .= " AND host ~* ?" if $filter; 1335 2050 # use alphaorder column for "correct" ordering of sort-by-type instead of DNS RR type number 1336 2051 $sql .= " ORDER BY $newsort $direction"; 1337 $sql .= " LIMIT $nrecs OFFSET ".($nstart*$nrecs) if $nstart ne 'all';1338 2052 1339 2053 my @bindvars = ($id); 1340 2054 push @bindvars, $filter if $filter; 2055 2056 # just to be ultraparanoid about SQL injection vectors 2057 if ($nstart ne 'all') { 2058 $sql .= " LIMIT ? OFFSET ?"; 2059 push @bindvars, $nrecs; 2060 push @bindvars, ($nstart*$nrecs); 2061 } 1341 2062 my $sth = $dbh->prepare($sql) or warn $dbh->errstr; 1342 2063 $sth->execute(@bindvars) or warn "$sql: ".$sth->errstr; … … 1353 2074 1354 2075 ## DNSDB::getRecCount() 1355 # Return count of non-SOA records in domain (or default records in a group) 1356 # Takes a database handle, default/live flag, group/domain ID, and optional filtering modifier 2076 # Return count of non-SOA records in zone (or default records in a group) 2077 # Takes a database handle, default/live flag, reverse/forward flag, group/domain ID, 2078 # and optional filtering modifier 1357 2079 # Returns the count 1358 2080 sub getRecCount { 1359 2081 my $dbh = shift; 1360 2082 my $defrec = shift; 2083 my $revrec = shift; 1361 2084 my $id = shift; 1362 2085 my $filter = shift || ''; … … 1368 2091 my @bindvars = ($id); 1369 2092 push @bindvars, $filter if $filter; 1370 my ($count) = $dbh->selectrow_array("SELECT count(*) FROM ".1371 ($defrec eq 'y' ? 'default_' : '')."records ".1372 "WHERE ".($defrec eq 'y' ? 'group' : 'domain')."_id=? ".1373 1374 ($filter ? " AND host ~* ?" : '') ,1375 2093 my $sql = "SELECT count(*) FROM ". 2094 _rectable($defrec,$revrec). 2095 " WHERE "._recparent($defrec,$revrec)."=? ". 2096 "AND NOT type=$reverse_typemap{SOA}". 2097 ($filter ? " AND host ~* ?" : ''); 2098 my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) ); 1376 2099 1377 2100 return $count; … … 1387 2110 # and weight/port for SRV 1388 2111 # Returns a status code and detail message in case of error 2112 ##fixme: pass a hash with the record data, not a series of separate values 1389 2113 sub addRec { 1390 2114 $errstr = ''; 1391 2115 my $dbh = shift; 1392 2116 my $defrec = shift; 1393 my $id = shift; 2117 my $revrec = shift; 2118 my $id = shift; # parent (group_id for defrecs, rdns_id for reverse records, 2119 # domain_id for domain records) 1394 2120 1395 2121 my $host = shift; 1396 my $rectype = shift; 2122 my $rectype = shift; # reference so we can coerce it if "+"-types can't find both zones 1397 2123 my $val = shift; 1398 2124 my $ttl = shift; … … 1418 2144 } 1419 2145 2146 my $domid = 0; 2147 my $revid = 0; 2148 2149 my $retcode = 'OK'; # assume everything will go OK 2150 my $retmsg = ''; 2151 2152 # do simple validation first 1420 2153 return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/; 1421 2154 1422 my $fields = ($defrec eq 'y' ? 'group_id' : 'domain_id').",host,type,val,ttl"; 1423 my $vallen = "?,?,?,?,?"; 1424 my @vallist = ($id,$host,$rectype,$val,$ttl); 1425 1426 my $dist; 1427 if ($rectype == $reverse_typemap{MX} or $rectype == $reverse_typemap{SRV}) { 1428 $dist = shift; 1429 return ('FAIL',"Distance is required for $typemap{$rectype} records") unless defined($dist); 1430 $dist =~ s/\s*//g; 1431 return ('FAIL',"Distance is required, and must be numeric") unless $dist =~ /^\d+$/; 1432 $fields .= ",distance"; 1433 $vallen .= ",?"; 1434 push @vallist, $dist; 1435 } 1436 my $weight; 1437 my $port; 1438 if ($rectype == $reverse_typemap{SRV}) { 1439 # check for _service._protocol. NB: RFC2782 does not say "MUST"... nor "SHOULD"... 1440 # it just says (paraphrased) "... is prepended with _ to prevent DNS collisions" 1441 return ('FAIL',"SRV records must begin with _service._protocol [$host]") 1442 unless $host =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/; 1443 $weight = shift; 1444 $port = shift; 1445 return ('FAIL',"Port and weight are required for SRV records") unless defined($weight) && defined($port); 1446 $weight =~ s/\s*//g; 1447 $port =~ s/\s*//g; 1448 return ('FAIL',"Port and weight are required, and must be numeric") 1449 unless $weight =~ /^\d+$/ && $port =~ /^\d+$/; 1450 $fields .= ",weight,port"; 1451 $vallen .= ",?,?"; 1452 push @vallist, ($weight,$port); 1453 } 2155 # Quick check on hostname parts. Note the regex is more forgiving than the error message; 2156 # domain names technically are case-insensitive, and we use printf-like % codes for a couple 2157 # of types. Other things may also be added to validate default records of several flavours. 2158 return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)") 2159 if $defrec eq 'n' && $$host !~ /^[0-9a-z_%.]+$/i; 2160 2161 # Collect these even if we're only doing a simple A record so we can call *any* validation sub 2162 my $dist = shift; 2163 my $port = shift; 2164 my $weight = shift; 2165 2166 my $fields; 2167 my @vallist; 2168 2169 # Call the validation sub for the type requested. 2170 ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id, 2171 host => $host, rectype => $rectype, val => $val, addr => $addr, 2172 dist => \$dist, port => \$port, weight => \$weight, 2173 fields => \$fields, vallist => \@vallist) ); 2174 2175 return ($retcode,$retmsg) if $retcode eq 'FAIL'; 2176 2177 # Set up database fields and bind parameters 2178 $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec); 2179 push @vallist, ($$host,$$rectype,$$val,$ttl,$id); 2180 my $vallen = '?'.(',?'x$#vallist); 1454 2181 1455 2182 # Allow transactions, and raise an exception on errors so we can catch it later. … … 1459 2186 1460 2187 eval { 1461 $dbh->do("INSERT INTO ". ($defrec eq 'y' ? 'default_' : '')."records($fields) VALUES ($vallen)",2188 $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)", 1462 2189 undef, @vallist); 1463 2190 $dbh->commit; … … 1469 2196 } 1470 2197 1471 return ( 'OK','OK');2198 return ($retcode, $retmsg); 1472 2199 1473 2200 } # end addRec() … … 1565 2292 my $dbh = shift; 1566 2293 my $defrec = shift; 2294 my $revrec = shift; 1567 2295 my $id = shift; 1568 2296 1569 my $sth = $dbh->prepare("DELETE FROM ". ($defrec eq 'y' ? 'default_' : '')."recordsWHERE record_id=?");2297 my $sth = $dbh->prepare("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?"); 1570 2298 $sth->execute($id); 1571 2299 … … 1577 2305 1578 2306 # Reference hashes. 1579 2307 my %par_tbl = ( 1580 2308 group => 'groups', 1581 2309 user => 'users', 1582 2310 defrec => 'default_records', 2311 defrevrec => 'default_rev_records', 1583 2312 domain => 'domains', 2313 revzone => 'revzones', 1584 2314 record => 'records' 1585 2315 ); 1586 2316 my %id_col = ( 1587 2317 group => 'group_id', 1588 2318 user => 'user_id', 1589 2319 defrec => 'record_id', 2320 defrevrec => 'record_id', 1590 2321 domain => 'domain_id', 2322 revzone => 'rdns_id', 1591 2323 record => 'record_id' 1592 2324 ); 1593 2325 my %par_col = ( 1594 2326 group => 'parent_group_id', 1595 2327 user => 'group_id', 1596 2328 defrec => 'group_id', 2329 defrevrec => 'group_id', 1597 2330 domain => 'group_id', 2331 revzone => 'group_id', 1598 2332 record => 'domain_id' 1599 2333 ); 1600 2334 my %par_type = ( 1601 2335 group => 'group', 1602 2336 user => 'group', 1603 2337 defrec => 'group', 2338 defrevrec => 'group', 1604 2339 domain => 'group', 2340 revzone => 'group', 1605 2341 record => 'domain' 1606 2342 ); 1607 2343 1608 ## DNSDB::getParents() 1609 # Find out which entities are parent to the requested id 1610 # Returns arrayref containing hash pairs of id/type 1611 sub getParents { 1612 my $dbh = shift; 1613 my $id = shift; 1614 my $type = shift; 1615 my $depth = shift || 'all'; # valid values: 'all', 'immed', <int> (stop at this group ID) 1616 1617 my @parlist; 1618 1619 while (1) { 1620 my $result = $dbh->selectrow_hashref("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?", 1621 undef, ($id) ); 1622 my %tmp = ($result->{$par_col{$type}} => $par_type{$type}); 1623 unshift @parlist, \%tmp; 1624 last if $result->{$par_col{$type}} == 1; # group 1 is its own parent 1625 $id = $result->{$par_col{$type}}; 1626 $type = $par_type{$type}; 1627 } 1628 1629 return \@parlist; 1630 1631 } # end getParents() 2344 2345 ## DNSDB::getTypelist() 2346 # Get a list of record types for various UI dropdowns 2347 # Takes database handle, forward/reverse/lookup flag, and optional "tag as selected" indicator (defaults to A) 2348 # Returns an arrayref to list of hashrefs perfect for HTML::Template 2349 sub getTypelist { 2350 my $dbh = shift; 2351 my $recgroup = shift; 2352 my $type = shift || $reverse_typemap{A}; 2353 2354 # also accepting $webvar{revrec}! 2355 $recgroup = 'f' if $recgroup eq 'n'; 2356 $recgroup = 'r' if $recgroup eq 'y'; 2357 2358 my $sql = "SELECT val,name FROM rectypes WHERE "; 2359 if ($recgroup eq 'r') { 2360 # reverse zone types 2361 $sql .= "stdflag=2 OR stdflag=3"; 2362 } elsif ($recgroup eq 'l') { 2363 # DNS lookup types. Note we avoid our custom types >= 65280, since those are entirely internal. 2364 $sql .= "(stdflag=1 OR stdflag=2 OR stdflag=3) AND val < 65280"; 2365 } else { 2366 # default; forward zone types. technically $type eq 'f' but not worth the error message. 2367 $sql .= "stdflag=1 OR stdflag=2"; 2368 } 2369 $sql .= " ORDER BY listorder"; 2370 2371 my $sth = $dbh->prepare($sql); 2372 $sth->execute; 2373 my @typelist; 2374 while (my ($rval,$rname) = $sth->fetchrow_array()) { 2375 my %row = ( recval => $rval, recname => $rname ); 2376 $row{tselect} = 1 if $rval == $type; 2377 push @typelist, \%row; 2378 } 2379 2380 # Add SOA on lookups since it's not listed in other dropdowns. 2381 if ($recgroup eq 'l') { 2382 my %row = ( recval => $reverse_typemap{SOA}, recname => 'SOA' ); 2383 $row{tselect} = 1 if $reverse_typemap{SOA} == $type; 2384 push @typelist, \%row; 2385 } 2386 2387 return \@typelist; 2388 } # end getTypelist() 2389 2390 2391 ## DNSDB::parentID() 2392 # Get ID of entity that is nearest parent to requested id 2393 # Takes a database handle and a hash of entity ID, entity type, optional parent type flag 2394 # (domain/reverse zone or group), and optional default/live and forward/reverse flags 2395 # Returns the ID or undef on failure 2396 sub parentID { 2397 my $dbh = shift; 2398 2399 my %args = @_; 2400 2401 # clean up the parent-type. Set it to group if not set; coerce revzone to domain for simpler logic 2402 $args{partype} = 'group' if !$args{partype}; 2403 $args{partype} = 'domain' if $args{partype} eq 'revzone'; 2404 2405 # clean up defrec and revrec. default to live record, forward zone 2406 $args{defrec} = 'n' if !$args{defrec}; 2407 $args{revrec} = 'n' if !$args{revrec}; 2408 2409 if ($par_type{$args{partype}} eq 'domain') { 2410 # only live records can have a domain/zone parent 2411 return unless ($args{type} eq 'record' && $args{defrec} eq 'n'); 2412 my $result = $dbh->selectrow_hashref("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id'). 2413 " FROM records WHERE record_id = ?", 2414 undef, ($args{id}) ) or return; 2415 return $result; 2416 } else { 2417 # snag some arguments that will either fall through or be overwritten to save some code duplication 2418 my $tmpid = $args{id}; 2419 my $type = $args{type}; 2420 if ($type eq 'record' && $args{defrec} eq 'n') { 2421 # Live records go through the records table first. 2422 ($tmpid) = $dbh->selectrow_array("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id'). 2423 " FROM records WHERE record_id = ?", 2424 undef, ($args{id}) ) or return; 2425 $type = ($args{revrec} eq 'n' ? 'domain' : 'revzone'); 2426 } 2427 my ($result) = $dbh->selectrow_array("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?", 2428 undef, ($tmpid) ); 2429 return $result; 2430 } 2431 # should be impossible to get here with even remotely sane arguments 2432 return; 2433 } # end parentID() 1632 2434 1633 2435 … … 1643 2445 1644 2446 # Return false on invalid types 1645 return 0 if !grep /^$type1$/, ('record','defrec',' user','domain','group');1646 return 0 if !grep /^$type2$/, ('record','defrec',' user','domain','group');2447 return 0 if !grep /^$type1$/, ('record','defrec','defrevrec','user','domain','revzone','group'); 2448 return 0 if !grep /^$type2$/, ('record','defrec','defrevrec','user','domain','revzone','group'); 1647 2449 1648 2450 # Return false on impossible relations 1649 2451 return 0 if $type1 eq 'record'; # nothing may be a child of a record 1650 2452 return 0 if $type1 eq 'defrec'; # nothing may be a child of a record 2453 return 0 if $type1 eq 'defrevrec'; # nothing may be a child of a record 1651 2454 return 0 if $type1 eq 'user'; # nothing may be child of a user 1652 2455 return 0 if $type1 eq 'domain' && $type2 ne 'record'; # domain may not be a parent of anything other than a record 2456 return 0 if $type1 eq 'revzone' && $type2 ne 'record';# reverse zone may not be a parent of anything other than a record 1653 2457 1654 2458 # ennnhhhh.... if we're passed an id of 0, it will never be found. usual 1655 2459 # case would be the UI creating a new <thing>, and so we don't have an ID for 1656 2460 # <thing> to look up yet. in that case the UI should check the parent as well. 1657 # argument for returning 1 is1658 2461 return 0 if $id1 == 0; # nothing can have a parent id of 0 1659 2462 return 1 if $id2 == 0; # anything could have a child id of 0 (or "unknown") … … 1665 2468 return 1 if $type1 eq 'group' && $type2 eq 'group' && $id1 == $id2; 1666 2469 1667 # almost the same loop as getParents() above1668 2470 my $id = $id2; 1669 2471 my $type = $type2; 1670 2472 my $foundparent = 0; 1671 2473 2474 # Records are the only entity with two possible parents. We need to split the parent checks on 2475 # domain/rdns. 2476 if ($type eq 'record') { 2477 my ($dom,$rdns) = $dbh->selectrow_array("SELECT domain_id,rdns_id FROM records WHERE record_id=?", 2478 undef, ($id)); 2479 # check immediate parent against request 2480 return 1 if $type1 eq 'domain' && $id1 == $dom; 2481 return 1 if $type1 eq 'revzone' && $id1 == $rdns; 2482 # if request is group, check *both* parents. Only check if the parent is nonzero though. 2483 return 1 if $dom && isParent($dbh, $id1, $type1, $dom, 'domain'); 2484 return 1 if $rdns && isParent($dbh, $id1, $type1, $rdns, 'revzone'); 2485 # exit here since we've executed the loop below by proxy in the above recursive calls. 2486 return 0; 2487 } 2488 2489 # almost the same loop as getParents() above 1672 2490 my $limiter = 0; 1673 2491 while (1) { … … 1677 2495 if (!$result) { 1678 2496 $limiter++; 1679 ##fixme: how often will this happen on a live site? 2497 ##fixme: how often will this happen on a live site? fail at max limiter <n>? 1680 2498 warn "no results looking for $sql with id $id (depth $limiter)\n"; 1681 2499 last; … … 1686 2504 } else { 1687 2505 ##fixme: do we care about trying to return a "no such record/domain/user/group" error? 2506 # should be impossible to create an inconsistent DB just with API calls. 1688 2507 warn $dbh->errstr." $sql, $id" if $dbh->errstr; 1689 2508 }
Note:
See TracChangeset
for help on using the changeset viewer.