source: trunk/DNSDB.pm@ 565

Last change on this file since 565 was 565, checked in by Kris Deugau, 11 years ago

/trunk

Edge case bug; databases upgraded from 1.0 to 1.2 schema will be missing
default reverse records. Fix the key bug of missing the SOA record by
adding one if it's missing on whatever entity one is editing. Other records
can be added as usual.

  • Property svn:keywords set to Date Rev Author Id
File size: 201.1 KB
RevLine 
[2]1# dns/trunk/DNSDB.pm
2# Abstraction functions for DNS administration
[262]3##
4# $Id: DNSDB.pm 565 2013-12-19 21:27:22Z kdeugau $
[496]5# Copyright 2008-2013 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
21package DNSDB;
22
23use strict;
24use warnings;
25use Exporter;
26use DBI;
[33]27use Net::DNS;
[65]28use Crypt::PasswdMD5;
[559]29use Digest::MD5 qw(md5_hex);
[198]30use Net::SMTP;
[539]31use NetAddr::IP 4.027 qw(:lower);
[198]32use POSIX;
[368]33use Fcntl qw(:flock);
[543]34use Time::TAI64 qw(:tai64);
[368]35
[2]36use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
37
[561]38$VERSION = 1.3; ##VERSION##
[2]39@ISA = qw(Exporter);
40@EXPORT_OK = qw(
[279]41 &initGlobals &login &initActionLog
[489]42 &getPermissions &changePermissions &comparePermissions
[112]43 &changeGroup
[517]44 &connectDB &finish
[276]45 &addDomain &delZone &domainName &revName &domainID &revID &addRDNS
[383]46 &getZoneCount &getZoneList &getZoneLocation
[22]47 &addGroup &delGroup &getChildren &groupName
[314]48 &getGroupCount &getGroupList
[83]49 &addUser &updateUser &delUser &userFullName &userStatus &getUserData
[326]50 &getUserCount &getUserList &getUserDropdown
[375]51 &addLoc &updateLoc &delLoc &getLoc
[370]52 &getLocCount &getLocList &getLocDropdown
[495]53 &getSOA &updateSOA &getRecLine &getRecList &getRecCount
[22]54 &addRec &updateRec &delRec
[323]55 &getLogCount &getLogEntries
[452]56 &getRevPattern
[225]57 &getTypelist
[254]58 &parentID
[117]59 &isParent
[452]60 &zoneStatus &getZonesByCIDR &importAXFR
[103]61 &export
[197]62 &mailNotify
[517]63 %typemap %reverse_typemap
[489]64 @permtypes $permlist %permchains
[2]65 );
66
[489]67@EXPORT = qw(%typemap %reverse_typemap @permtypes $permlist %permchains);
[2]68%EXPORT_TAGS = ( ALL => [qw(
[279]69 &initGlobals &login &initActionLog
[489]70 &getPermissions &changePermissions &comparePermissions
[112]71 &changeGroup
[517]72 &connectDB &finish
[276]73 &addDomain &delZone &domainName &revName &domainID &revID &addRDNS
[383]74 &getZoneCount &getZoneList &getZoneLocation
[22]75 &addGroup &delGroup &getChildren &groupName
[314]76 &getGroupCount &getGroupList
[83]77 &addUser &updateUser &delUser &userFullName &userStatus &getUserData
[326]78 &getUserCount &getUserList &getUserDropdown
[375]79 &addLoc &updateLoc &delLoc &getLoc
[370]80 &getLocCount &getLocList &getLocDropdown
[495]81 &getSOA &updateSOA &getRecLine &getRecList &getRecCount
[22]82 &addRec &updateRec &delRec
[323]83 &getLogCount &getLogEntries
[452]84 &getRevPattern
[225]85 &getTypelist
[254]86 &parentID
[117]87 &isParent
[452]88 &zoneStatus &getZonesByCIDR &importAXFR
[103]89 &export
[197]90 &mailNotify
[517]91 %typemap %reverse_typemap
[489]92 @permtypes $permlist %permchains
[2]93 )]
94 );
95
96our $errstr = '';
[283]97our $resultstr = '';
[2]98
[66]99# Arguably defined wholly in the db, but little reason to change without supporting code changes
[370]100# group_view, user_view permissions? separate rDNS permission(s)?
[66]101our @permtypes = qw (
102 group_edit group_create group_delete
103 user_edit user_create user_delete
104 domain_edit domain_create domain_delete
[387]105 record_edit record_create record_delete record_locchg
[370]106 location_edit location_create location_delete location_view
[66]107 self_edit admin
108);
109our $permlist = join(',',@permtypes);
110
[390]111# Some permissions more or less require certain others.
112our %permchains = (
113 user_edit => 'self_edit',
114 location_edit => 'location_view',
115 location_create => 'location_view',
116 location_delete => 'location_view',
117 record_locchg => 'location_view',
118);
119
[2]120# DNS record type map and reverse map.
121# loaded from the database, from http://www.iana.org/assignments/dns-parameters
122our %typemap;
123our %reverse_typemap;
124
[228]125## (Semi)private variables
[278]126
[228]127# Hash of functions for validating record types. Filled in initGlobals() since
128# it relies on visibility flags from the rectypes table in the DB
129my %validators;
[128]130
[295]131# Entity-relationship reference hashes.
132my %par_tbl = (
133 group => 'groups',
134 user => 'users',
135 defrec => 'default_records',
136 defrevrec => 'default_rev_records',
137 domain => 'domains',
138 revzone => 'revzones',
139 record => 'records'
140 );
141my %id_col = (
142 group => 'group_id',
143 user => 'user_id',
144 defrec => 'record_id',
145 defrevrec => 'record_id',
146 domain => 'domain_id',
147 revzone => 'rdns_id',
148 record => 'record_id'
149 );
150my %par_col = (
151 group => 'parent_group_id',
152 user => 'group_id',
153 defrec => 'group_id',
154 defrevrec => 'group_id',
155 domain => 'group_id',
156 revzone => 'group_id',
157 record => 'domain_id'
158 );
159my %par_type = (
160 group => 'group',
161 user => 'group',
162 defrec => 'group',
163 defrevrec => 'group',
164 domain => 'group',
165 revzone => 'group',
166 record => 'domain'
167 );
[278]168
[2]169##
[465]170## Constructor and destructor
171##
172
173sub new {
174 my $this = shift;
175 my $class = ref($this) || $this;
176 my %args = @_;
[517]177
178 # Prepopulate a basic config. Note some of these *will* cause errors if left unset.
179 # note: add appropriate stanzas in __cfgload() to parse these
180 my %defconfig = (
[529]181 # The only configuration options not loadable from a config file.
182 configfile => "/etc/dnsdb/dnsdb.conf", ##CFG_LEAF##
[517]183
184 # Database connection info
185 dbname => 'dnsdb',
186 dbuser => 'dnsdb',
187 dbpass => 'secret',
188 dbhost => '',
189
190 # Email notice settings
191 mailhost => 'smtp.example.com',
192 mailnotify => 'dnsdb@example.com', # to
193 mailsender => 'dnsdb@example.com', # from
194 mailname => 'DNS Administration',
195 orgname => 'Example Corp',
196 domain => 'example.com',
197
198 # Template directory
199 templatedir => 'templates/',
200# fmeh. this is a real web path, not a logical internal one. hm..
201# cssdir => 'templates/',
202 sessiondir => 'session/',
203 exportcache => 'cache/',
[559]204 usecache => 1, # do we bother using the cache above?
[517]205
206 # Session params
207 timeout => '1h', # passed as-is to CGI::Session
208
209 # Other miscellanea
210 log_failures => 1, # log all evarthing by default
211 perpage => 15,
[559]212 maxfcgi => 10, # reasonable default?
[524]213 force_refresh => 1,
[542]214 lowercase => 0, # mangle as little as possible by default
[517]215 );
216
217 # Config file parse calls.
218 # If we are passed a blank argument for $args{configfile},
219 # we should NOT parse the default config file - we will
220 # rely on hardcoded defaults OR caller-specified values.
221 # If we are passed a non-blank argument, parse that file.
222 # If no config file is specified, parse the default one.
223 my %siteconfig;
224 if (defined($args{configfile})) {
225 if ($args{configfile}) {
226 return if !__cfgload($args{configfile}, \%siteconfig);
227 }
228 } else {
229 return if !__cfgload($defconfig{configfile}, \%siteconfig);
[465]230 }
[517]231
232 # Assemble the object. Apply configuration hashes in order of precedence.
233 my $self = {
234 # Hardcoded defaults
235 %defconfig,
236 # Default config file OR caller-specified one, loaded above
237 %siteconfig,
238 # Caller-specified arguments
239 %args
240 };
[465]241 bless $self, $class;
[517]242
[524]243 # Several settings are booleans. Handle multiple possible ways of setting them.
[560]244 for my $boolopt ('log_failures', 'force_refresh', 'lowercase', 'usecache') {
[524]245 if ($self->{$boolopt} ne '1' && $self->{$boolopt} ne '0') {
246 # true/false, on/off, yes/no all valid.
247 if ($self->{$boolopt} =~ /^(?:true|false|t|f|on|off|yes|no)$/) {
248 if ($self->{$boolopt} =~ /(?:true|t|on|yes)/) {
249 $self->{$boolopt} = 1;
250 } else {
251 $self->{$boolopt} = 0;
252 }
253 } else {
254 warn "Bad $boolopt setting $self->{$boolopt}\n";
255 $self->{$boolopt} = 1;
256 }
257 }
258 }
259
[517]260 # Try to connect to the DB, and initialize a number of handy globals.
[465]261 $self->{dbh} = connectDB($self->{dbname}, $self->{dbuser}, $self->{dbpass}, $self->{dbhost}) or return;
262 $self->initGlobals();
263
264 return $self;
265}
266
267sub DESTROY {
268 my $self = shift;
[517]269 $self->{dbh}->disconnect if $self->{dbh};
[465]270}
271
[495]272sub errstr { $DNSDB::errstr; }
273
[465]274##
[225]275## utility functions
[281]276##
277
278## DNSDB::_rectable()
[224]279# Takes default+rdns flags, returns appropriate table name
280sub _rectable {
281 my $def = shift;
282 my $rev = shift;
283
284 return 'records' if $def ne 'y';
285 return 'default_records' if $rev ne 'y';
286 return 'default_rev_records';
287} # end _rectable()
288
[281]289## DNSDB::_recparent()
[224]290# Takes default+rdns flags, returns appropriate parent-id column name
291sub _recparent {
292 my $def = shift;
293 my $rev = shift;
294
295 return 'group_id' if $def eq 'y';
296 return 'rdns_id' if $rev eq 'y';
297 return 'domain_id';
298} # end _recparent()
299
[281]300## DNSDB::_ipparent()
[226]301# Check an IP to be added in a reverse zone to see if it's really in the requested parent.
[466]302# Takes default and reverse flags, IP (fragment) to check, parent zone ID,
[226]303# and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for
304# database insertion)
305sub _ipparent {
[466]306 my $self = shift;
307 my $dbh = $self->{dbh};
[226]308 my $defrec = shift;
309 my $revrec = shift;
310 my $val = shift;
311 my $id = shift;
312 my $addr = shift;
[224]313
[232]314 return if $revrec ne 'y'; # this sub not useful in forward zones
315
316 $$addr = NetAddr::IP->new($$val); #necessary?
317
[226]318 # subsub to split, reverse, and overlay an IP fragment on a netblock
319 sub __rev_overlay {
320 my $splitme = shift; # ':' or '.', m'lud?
321 my $parnet = shift;
322 my $val = shift;
323 my $addr = shift;
324
325 my $joinme = $splitme;
326 $splitme = '\.' if $splitme eq '.';
[232]327 my @working = reverse(split($splitme, $parnet->addr));
328 my @parts = reverse(split($splitme, $$val));
[226]329 for (my $i = 0; $i <= $#parts; $i++) {
330 $working[$i] = $parts[$i];
331 }
[232]332 my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0;
333 return 0 unless $checkme->within($parnet);
[226]334 $$addr = $checkme; # force "correct" IP to be recorded.
335 return 1;
336 }
337
338 my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id));
339 my $parnet = NetAddr::IP->new($parstr);
340
341 # Fail early on v6-in-v4 or v4-in-v6. We're not accepting these ATM.
[232]342 return 0 if $parnet->addr =~ /\./ && $$val =~ /:/;
343 return 0 if $parnet->addr =~ /:/ && $$val =~ /\./;
[226]344
[340]345 if ($$addr && ($$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/ || $$val =~ m|/\d+$|)) {
346 # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address,
347 # or a netblock (only expected on templates)
[232]348 # the rest we have to restructure before fiddling. *sigh*
349 return 1 if $$addr->within($parnet);
350 } else {
[340]351 # We don't have a complete IP in $$val (yet)... unless we have a netblock
[232]352 if ($parnet->addr =~ /:/) {
353 $$val =~ s/^:+//; # gotta strip'em all...
[226]354 return __rev_overlay(':', $parnet, $val, $addr);
355 }
[232]356 if ($parnet->addr =~ /\./) {
357 $$val =~ s/^\.+//;
358 return __rev_overlay('.', $parnet, $val, $addr);
359 }
[226]360 # should be impossible to get here...
361 }
362 # ... and here.
363 # can't do nuttin' in forward zones
364} # end _ipparent()
365
[281]366## DNSDB::_hostparent()
[232]367# A little different than _ipparent above; this tries to *find* the parent zone of a hostname
[466]368# Takes a hostname.
[281]369# Returns the domain ID of the parent domain if one was found.
[232]370sub _hostparent {
[466]371 my $self = shift;
372 my $dbh = $self->{dbh};
[232]373 my $hname = shift;
[347]374
375 $hname =~ s/^\*\.//; # this should be impossible to find in the domains table.
[232]376 my @hostbits = split /\./, $hname;
[349]377 my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE lower(domain) = lower(?) GROUP BY domain_id");
[232]378 foreach (@hostbits) {
379 $sth->execute($hname);
380 my ($found, $parid) = $sth->fetchrow_array;
381 if ($found) {
382 return $parid;
383 }
384 $hname =~ s/^$_\.//;
385 }
386} # end _hostparent()
[228]387
[281]388## DNSDB::_log()
389# Log an action
[466]390# Takes a log entry hash containing at least:
[282]391# group_id, log entry
[281]392# and optionally one or more of:
393# domain_id, rdns_id
[282]394# The %userdata hash provides the user ID, username, and fullname
[281]395sub _log {
[466]396 my $self = shift;
397 my $dbh = $self->{dbh};
[281]398
399 my %args = @_;
400
401 $args{rdns_id} = 0 if !$args{rdns_id};
402 $args{domain_id} = 0 if !$args{domain_id};
403
404##fixme: farm out the actual logging to different subs for file, syslog, internal, etc based on config
[517]405# if ($self->{log_channel} eq 'sql') {
[281]406 $dbh->do("INSERT INTO log (domain_id,rdns_id,group_id,entry,user_id,email,name) VALUES (?,?,?,?,?,?,?)",
407 undef,
408 ($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{entry},
[488]409 $self->{loguserid}, $self->{logusername}, $self->{logfullname}) );
[517]410# } elsif ($self->{log_channel} eq 'file') {
411# } elsif ($self->{log_channel} eq 'syslog') {
[281]412# }
413} # end _log
414
415
[224]416##
[228]417## Record validation subs.
418##
419
[281]420## All of these subs take substantially the same arguments:
421# a hash containing at least the following keys:
422# - defrec (default/live flag)
423# - revrec (forward/reverse flag)
424# - id (parent entity ID)
425# - host (hostname)
426# - rectype
427# - val (IP, hostname [CNAME/MX/SRV] or text)
428# - addr (NetAddr::IP object from val. May be undef.)
429# MX and SRV record validation also expect distance, and SRV records expect weight and port as well.
430# host, rectype, and addr should be references as these may be modified in validation
431
[228]432# A record
433sub _validate_1 {
[467]434 my $self = shift;
435 my $dbh = $self->{dbh};
[229]436
[230]437 my %args = @_;
[229]438
[230]439 return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y';
[229]440
441 # Coerce all hostnames to end in ".DOMAIN" for group/default records,
442 # or the intended parent domain for live records.
[467]443 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
[531]444 ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
[229]445
446 # Check IP is well-formed, and that it's a v4 address
[234]447 # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
[232]448 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
[234]449 unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/;
450 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
[230]451 unless $args{addr} && !$args{addr}->{isv6};
[229]452 # coerce IP/value to normalized form for storage
[230]453 ${$args{val}} = $args{addr}->addr;
[229]454
[228]455 return ('OK','OK');
456} # done A record
457
458# NS record
459sub _validate_2 {
[467]460 my $self = shift;
461 my $dbh = $self->{dbh};
[230]462
463 my %args = @_;
464
[328]465 # Check that the target of the record is within the parent.
466 # Yes, host<->val are mixed up here; can't see a way to avoid it. :(
467 if ($args{defrec} eq 'n') {
468 # Check if IP/address/zone/"subzone" is within the parent
469 if ($args{revrec} eq 'y') {
470 my $tmpip = NetAddr::IP->new(${$args{val}});
[467]471 my $pname = $self->revName($args{id});
[328]472 return ('FAIL',"${$args{val}} not within $pname")
[467]473 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
[328]474 # Sub the returned thing for ZONE? This could get stupid if you have typos...
475 ${$args{val}} =~ s/ZONE/$tmpip->address/;
476 } else {
[467]477 my $pname = $self->domainName($args{id});
[328]478 ${$args{host}} = $pname if ${$args{host}} !~ /\.$pname$/;
479 }
[230]480 } else {
[328]481 # Default reverse NS records should always refer to the implied parent
482 ${$args{host}} = 'DOMAIN' if $args{revrec} eq 'n';
483 ${$args{val}} = 'ZONE' if $args{revrec} eq 'y';
[230]484 }
485
486# Let this lie for now. Needs more magic.
487# # Check IP is well-formed, and that it's a v4 address
488# return ('FAIL',"A record must be a valid IPv4 address")
489# unless $addr && !$addr->{isv6};
490# # coerce IP/value to normalized form for storage
491# $$val = $addr->addr;
492
[228]493 return ('OK','OK');
494} # done NS record
495
496# CNAME record
497sub _validate_5 {
[467]498 my $self = shift;
499 my $dbh = $self->{dbh};
[230]500
501 my %args = @_;
502
503# Not really true, but these are only useful for delegating smaller-than-/24 IP blocks.
504# This is fundamentally a messy operation and should really just be taken care of by the
505# export process, not manual maintenance of the necessary records.
506 return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y';
507
508 # Coerce all hostnames to end in ".DOMAIN" for group/default records,
509 # or the intended parent domain for live records.
[467]510 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
[230]511 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
512
[228]513 return ('OK','OK');
514} # done CNAME record
515
516# SOA record
517sub _validate_6 {
[230]518 # Smart monkeys won't stick their fingers in here; we have
519 # separate dedicated routines to deal with SOA records.
[228]520 return ('OK','OK');
521} # done SOA record
522
523# PTR record
524sub _validate_12 {
[467]525 my $self = shift;
526 my $dbh = $self->{dbh};
[232]527
528 my %args = @_;
529
530 if ($args{revrec} eq 'y') {
531 if ($args{defrec} eq 'n') {
[467]532 return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".$self->revName($args{id}))
533 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
[232]534 ${$args{val}} = $args{addr}->addr;
535 } else {
[234]536 if (${$args{val}} =~ /\./) {
537 # looks like a v4 or fragment
538 if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) {
539 # woo! a complete IP! validate it and normalize, or fail.
540 $args{addr} = NetAddr::IP->new(${$args{val}})
541 or return ('FAIL', "IP/value looks like IPv4 but isn't valid");
542 ${$args{val}} = $args{addr}->addr;
543 } else {
[249]544 ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/;
[234]545 }
546 } elsif (${$args{val}} =~ /[a-f:]/) {
547 # looks like a v6 or fragment
[251]548 ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/;
[234]549 if ($args{addr}) {
550 if ($args{addr}->addr =~ /^0/) {
[251]551 ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/;
[234]552 } else {
553 ${$args{val}} = $args{addr}->addr;
554 }
555 }
556 } else {
557 # bare number (probably). These could be v4 or v6, so we'll
558 # expand on these on creation of a reverse zone.
[251]559 ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/;
[234]560 }
[517]561 ${$args{host}} =~ s/\.*$/\.$self->{domain}/ if ${$args{host}} !~ /(?:$self->{domain}|ADMINDOMAIN)$/;
[232]562 }
563
564# Multiple PTR records do NOT generally do what most people believe they do,
565# and tend to fail in the most awkward way possible. Check and warn.
566# We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12"
[249]567
568 my @checkvals = (${$args{val}});
569 if (${$args{val}} =~ /,/) {
570 # push . and :: variants into checkvals if val has ,
571 my $tmp;
572 ($tmp = ${$args{val}}) =~ s/,/./;
573 push @checkvals, $tmp;
574 ($tmp = ${$args{val}}) =~ s/,/::/;
575 push @checkvals, $tmp;
576 }
577 my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?");
578 foreach my $checkme (@checkvals) {
[272]579 if ($args{update}) {
580 # Record update. There should usually be an existing PTR (the record being updated)
581 my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}).
582 " WHERE val = ?", undef, ($checkme)) };
583 return ('WARN', "PTR record for $checkme already exists; adding another will probably not do what you want")
[273]584 if @ptrs && (!grep /^$args{update}$/, @ptrs);
[272]585 } else {
586 # New record. Always warn if a PTR exists
587 my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
588 " WHERE val = ?", undef, ($checkme));
589 return ('WARN', "PTR record for $checkme already exists; adding another will probably not do what you want")
590 if $ptrcount;
591 }
[249]592 }
[272]593
[232]594 } else {
595 # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations
596 # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct
597 # PTR records on export
598 return ('FAIL',"Forward zones cannot contain PTR records");
599 }
600
[228]601 return ('OK','OK');
602} # done PTR record
603
604# MX record
605sub _validate_15 {
[467]606 my $self = shift;
607 my $dbh = $self->{dbh};
[230]608
609 my %args = @_;
610
611# Not absolutely true but WTF use is an MX record for a reverse zone?
612 return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y';
613
614 return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}});
615 ${$args{dist}} =~ s/\s*//g;
616 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
617
618 ${$args{fields}} = "distance,";
619 push @{$args{vallist}}, ${$args{dist}};
620
621 # Coerce all hostnames to end in ".DOMAIN" for group/default records,
622 # or the intended parent domain for live records.
[467]623 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
[230]624 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
625
[273]626# hmm.. this might work. except possibly for something pointing to "deadbeef.ca". <g>
627# if ($type == $reverse_typemap{NS} || $type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) {
628# if ($val =~ /^\s*[\da-f:.]+\s*$/) {
629# return ('FAIL',"$val is not a valid IP address") if !$addr;
630# }
631# }
632
[228]633 return ('OK','OK');
634} # done MX record
635
636# TXT record
637sub _validate_16 {
[231]638 # Could arguably put a WARN return here on very long (>512) records
[228]639 return ('OK','OK');
640} # done TXT record
641
642# RP record
643sub _validate_17 {
[231]644 # Probably have to validate these some day
[228]645 return ('OK','OK');
646} # done RP record
647
648# AAAA record
649sub _validate_28 {
[467]650 my $self = shift;
651 my $dbh = $self->{dbh};
[229]652
[230]653 my %args = @_;
[229]654
[230]655 return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y';
[229]656
657 # Coerce all hostnames to end in ".DOMAIN" for group/default records,
658 # or the intended parent domain for live records.
[467]659 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
[230]660 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
[229]661
662 # Check IP is well-formed, and that it's a v6 address
[232]663 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address")
[230]664 unless $args{addr} && $args{addr}->{isv6};
[229]665 # coerce IP/value to normalized form for storage
[230]666 ${$args{val}} = $args{addr}->addr;
[229]667
[228]668 return ('OK','OK');
669} # done AAAA record
670
671# SRV record
672sub _validate_33 {
[467]673 my $self = shift;
674 my $dbh = $self->{dbh};
[231]675
676 my %args = @_;
677
678# Not absolutely true but WTF use is an SRV record for a reverse zone?
679 return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y';
680
681 return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}});
682 ${$args{dist}} =~ s/\s*//g;
683 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
684
685 return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
686 unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
687 return ('FAIL',"Port and weight are required for SRV records")
688 unless defined(${$args{weight}}) && defined(${$args{port}});
689 ${$args{weight}} =~ s/\s*//g;
690 ${$args{port}} =~ s/\s*//g;
691
692 return ('FAIL',"Port and weight are required, and must be numeric")
693 unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/;
694
695 ${$args{fields}} = "distance,weight,port,";
696 push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}});
697
698 # Coerce all hostnames to end in ".DOMAIN" for group/default records,
699 # or the intended parent domain for live records.
[467]700 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
[231]701 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
702
[228]703 return ('OK','OK');
704} # done SRV record
705
706# Now the custom types
707
[232]708# A+PTR record. With a very little bit of magic we can also use this sub to validate AAAA+PTR. Whee!
[228]709sub _validate_65280 {
[467]710 my $self = shift;
711 my $dbh = $self->{dbh};
[232]712
713 my %args = @_;
714
715 my $code = 'OK';
716 my $msg = 'OK';
717
718 if ($args{defrec} eq 'n') {
719 # live record; revrec determines whether we validate the PTR or A component first.
[233]720
[232]721 if ($args{revrec} eq 'y') {
[467]722 ($code,$msg) = $self->_validate_12(%args);
[232]723 return ($code,$msg) if $code eq 'FAIL';
724
[467]725 # check A+PTR is really v4
726 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
727 if ${$args{rectype}} == 65280 && $args{addr}->{isv6};
728 # check AAAA+PTR is really v6
729 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address")
730 if ${$args{rectype}} == 65281 && !$args{addr}->{isv6};
731
[232]732 # Check if the reqested domain exists. If not, coerce the type down to PTR and warn.
[467]733 if (!(${$args{domid}} = $self->_hostparent(${$args{host}}))) {
[272]734 my $addmsg = "Record ".($args{update} ? 'updated' : 'added').
735 " as PTR instead of $typemap{${$args{rectype}}}; domain not found for ${$args{host}}";
[232]736 $msg .= "\n$addmsg" if $code eq 'WARN';
737 $msg = $addmsg if $code eq 'OK';
738 ${$args{rectype}} = $reverse_typemap{PTR};
739 return ('WARN', $msg);
740 }
741
[242]742 # Add domain ID to field list and values
743 ${$args{fields}} .= "domain_id,";
744 push @{$args{vallist}}, ${$args{domid}};
745
[232]746 } else {
[467]747 ($code,$msg) = $self->_validate_1(%args) if ${$args{rectype}} == 65280;
748 ($code,$msg) = $self->_validate_28(%args) if ${$args{rectype}} == 65281;
[232]749 return ($code,$msg) if $code eq 'FAIL';
750
751 # Check if the requested reverse zone exists - note, an IP fragment won't
752 # work here since we don't *know* which parent to put it in.
[233]753 # ${$args{val}} has been validated as a valid IP by now, in one of the above calls.
[232]754 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?".
755 " ORDER BY masklen(revnet) DESC", undef, (${$args{val}}));
756 if (!$revid) {
[272]757 $msg = "Record ".($args{update} ? 'updated' : 'added')." as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA').
[232]758 " instead of $typemap{${$args{rectype}}}; reverse zone not found for ${$args{val}}";
759 ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA});
760 return ('WARN', $msg);
761 }
762
763 # Check for duplicate PTRs. Note we don't have to play games with $code and $msg, because
764 # by definition there can't be duplicate PTRs if the reverse zone isn't managed here.
[272]765 if ($args{update}) {
766 # Record update. There should usually be an existing PTR (the record being updated)
767 my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}).
768 " WHERE val = ?", undef, (${$args{val}})) };
[273]769 if (@ptrs && (!grep /^$args{update}$/, @ptrs)) {
[272]770 $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want";
771 $code = 'WARN';
772 }
773 } else {
774 # New record. Always warn if a PTR exists
775 my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
776 " WHERE val = ?", undef, (${$args{val}}));
777 $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want"
778 if $ptrcount;
779 $code = 'WARN' if $ptrcount;
[232]780 }
781
[272]782# my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
783# " WHERE val = ?", undef, ${$args{val}});
784# if ($ptrcount) {
785# my $curid = $dbh->selectrow_array("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}).
786# " WHERE val = ?
787# $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want";
788# $code = 'WARN';
789# }
790
[232]791 ${$args{fields}} .= "rdns_id,";
792 push @{$args{vallist}}, $revid;
793 }
794
[233]795 } else { # defrec eq 'y'
796 if ($args{revrec} eq 'y') {
[467]797 ($code,$msg) = $self->_validate_12(%args);
[233]798 return ($code,$msg) if $code eq 'FAIL';
799 if (${$args{rectype}} == 65280) {
800 return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment")
801 if ${$args{val}} =~ /:/;
[234]802 ${$args{val}} =~ s/^ZONE,/ZONE./; # Clean up after uncertain IP-fragment-type from _validate_12
[233]803 } elsif (${$args{rectype}} == 65281) {
804 return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment")
805 if ${$args{val}} =~ /\./;
[234]806 ${$args{val}} =~ s/^ZONE,/ZONE::/; # Clean up after uncertain IP-fragment-type from _validate_12
[233]807 }
808 } else {
809 # This is easy. I also can't see a real use-case for A/AAAA+PTR in *all* forward
810 # domains, since you wouldn't be able to substitute both domain and reverse zone
811 # sanely, and you'd end up with guaranteed over-replicated PTR records that would
812 # confuse the hell out of pretty much anything that uses them.
[234]813##fixme: make this a config flag?
[233]814 return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains");
815 }
[232]816 }
817
818 return ($code, $msg);
[228]819} # done A+PTR record
820
821# AAAA+PTR record
[232]822# A+PTR above has been magicked to handle AAAA+PTR as well.
[228]823sub _validate_65281 {
[232]824 return _validate_65280(@_);
[228]825} # done AAAA+PTR record
826
827# PTR template record
828sub _validate_65282 {
[467]829 my $self = shift;
830 my $dbh = $self->{dbh};
[340]831
832 my %args = @_;
833
834 # we're *this* >.< close to being able to just call _validate_12... unfortunately we can't, quite.
835 if ($args{revrec} eq 'y') {
836 if ($args{defrec} eq 'n') {
[467]837 return ('FAIL', "Template block ${$args{val}} is not within ".$self->revName($args{id}))
838 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
[340]839##fixme: warn if $args{val} is not /31 or larger block?
840 ${$args{val}} = "$args{addr}";
841 } else {
842 if (${$args{val}} =~ /\./) {
843 # looks like a v4 or fragment
844 if (${$args{val}} =~ m|^\d+\.\d+\.\d+\.\d+(?:/\d+)?$|) {
845 # woo! a complete IP! validate it and normalize, or fail.
846 $args{addr} = NetAddr::IP->new(${$args{val}})
847 or return ('FAIL', "IP/value looks like IPv4 but isn't valid");
848 ${$args{val}} = "$args{addr}";
849 } else {
850 ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/;
851 }
852 } elsif (${$args{val}} =~ /[a-f:]/) {
853 # looks like a v6 or fragment
854 ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/;
855 if ($args{addr}) {
856 if ($args{addr}->addr =~ /^0/) {
857 ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/;
858 } else {
859 ${$args{val}} = "$args{addr}";
860 }
861 }
862 } else {
863 # bare number (probably). These could be v4 or v6, so we'll
864 # expand on these on creation of a reverse zone.
865 ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/;
866 }
867 }
868##fixme: validate %-patterns?
869
870# Unlike single PTR records, there is absolutely no way to sanely support multiple
871# PTR templates for the same block, since they expect to expand to all the individual
872# IPs on export. Nested templates should be supported though.
873
874 my @checkvals = (${$args{val}});
875 if (${$args{val}} =~ /,/) {
876 # push . and :: variants into checkvals if val has ,
877 my $tmp;
878 ($tmp = ${$args{val}}) =~ s/,/./;
879 push @checkvals, $tmp;
880 ($tmp = ${$args{val}}) =~ s/,/::/;
881 push @checkvals, $tmp;
882 }
883##fixme: this feels wrong still - need to restrict template pseudorecords to One Of Each
884# Per Netblock such that they don't conflict on export
885 my $typeck;
886# type 65282 -> ptr template -> look for any of 65282, 65283, 65284
887 $typeck = 'type=65283 OR type=65284' if ${$args{rectype}} == 65282;
888# type 65283 -> a+ptr template -> v4 -> look for 65282 or 65283
889 $typeck = 'type=65283' if ${$args{rectype}} == 65282;
890# type 65284 -> aaaa+ptr template -> v6 -> look for 65282 or 65284
891 $typeck = 'type=65284' if ${$args{rectype}} == 65282;
892 my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ? ".
893 "AND (type=65282 OR $typeck)");
894 foreach my $checkme (@checkvals) {
895 $pcsth->execute($checkme);
896 my ($rc) = $pcsth->fetchrow_array;
[442]897 return ('FAIL', "Only one template pseudorecord may exist for a given IP block") if $rc > 1;
[340]898 }
899
900 } else {
901 return ('FAIL', "Forward zones cannot contain PTR records");
902 }
903
[228]904 return ('OK','OK');
905} # done PTR template record
906
907# A+PTR template record
908sub _validate_65283 {
[467]909 my $self = shift;
910 my $dbh = $self->{dbh};
[340]911
912 my %args = @_;
913
914 my ($code,$msg) = ('OK','OK');
915
916##fixme: need to fiddle things since A+PTR templates are acceptable in live
917# forward zones but not default records
918 if ($args{defrec} eq 'n') {
919 if ($args{revrec} eq 'n') {
[467]920 ($code,$msg) = $self->_validate_1(%args) if ${$args{rectype}} == 65280;
921 ($code,$msg) = $self->_validate_28(%args) if ${$args{rectype}} == 65281;
[340]922 return ($code,$msg) if $code eq 'FAIL';
923
924 # Check if the requested reverse zone exists - note, an IP fragment won't
925 # work here since we don't *know* which parent to put it in.
926 # ${$args{val}} has been validated as a valid IP by now, in one of the above calls.
927 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?".
928 " ORDER BY masklen(revnet) DESC", undef, (${$args{val}}));
929 # Fail if no match; we can't coerce a PTR-template type down to not include the PTR bit currently.
930 if (!$revid) {
931 $msg = "Can't ".($args{update} ? 'update' : 'add')." ${$args{host}}/${$args{val}} as ".
932 "$typemap{${$args{rectype}}}: reverse zone not found for ${$args{val}}";
933##fixme: add A template, AAAA template types?
934# ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA});
935 return ('FAIL', $msg);
936 }
937
938 # Add reverse zone ID to field list and values
939 ${$args{fields}} .= "rdns_id,";
940 push @{$args{vallist}}, $revid;
941
942 } else {
[467]943 return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".$self->revName($args{id}))
944 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
[340]945 ${$args{val}} = "$args{addr}";
946
[467]947 if (!(${$args{domid}} = $self->_hostparent(${$args{host}}))) {
[340]948 my $addmsg = "Record ".($args{update} ? 'updated' : 'added').
949 " as PTR template instead of $typemap{${$args{rectype}}}; domain not found for ${$args{host}}";
950 $msg .= "\n$addmsg" if $code eq 'WARN';
951 $msg = $addmsg if $code eq 'OK';
952 ${$args{rectype}} = 65282;
953 return ('WARN', $msg);
954 }
955
956 # Add domain ID to field list and values
957 ${$args{fields}} .= "domain_id,";
958 push @{$args{vallist}}, ${$args{domid}};
959 }
960
961 } else {
[467]962 my ($code,$msg) = $self->_validate_65282(%args);
[340]963 return ($code, $msg) if $code eq 'FAIL';
964 # get domain, check against ${$args{name}}
965 }
966
[228]967 return ('OK','OK');
968} # done AAAA+PTR template record
969
970# AAAA+PTR template record
971sub _validate_65284 {
972 return ('OK','OK');
973} # done AAAA+PTR template record
974
[342]975# Delegation record
[345]976# This is essentially a specialized clone of the NS record, primarily useful
977# for delegating IPv4 sub-/24 reverse blocks
[342]978sub _validate_65285 {
[467]979 my $self = shift;
980 my $dbh = $self->{dbh};
[345]981
982 my %args = @_;
983
984# Almost, but not quite, identical to NS record validation.
985
986 # Check that the target of the record is within the parent.
987 # Yes, host<->val are mixed up here; can't see a way to avoid it. :(
988 if ($args{defrec} eq 'n') {
989 # Check if IP/address/zone/"subzone" is within the parent
990 if ($args{revrec} eq 'y') {
991 my $tmpip = NetAddr::IP->new(${$args{val}});
[467]992 my $pname = $self->revName($args{id});
[345]993 return ('FAIL',"${$args{val}} not within $pname")
[467]994 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
[345]995 # Normalize
996 ${$args{val}} = "$tmpip";
997 } else {
[467]998 my $pname = $self->domainName($args{id});
[345]999 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
1000 }
1001 } else {
1002 return ('FAIL',"Delegation records are not permitted in default record sets");
1003 }
[342]1004 return ('OK','OK');
1005}
[228]1006
[342]1007
[265]1008##
1009## Record data substitution subs
1010##
[228]1011
[298]1012# Replace ZONE in hostname, or create (most of) the actual proper zone name
[265]1013sub _ZONE {
1014 my $zone = shift;
1015 my $string = shift;
1016 my $fr = shift || 'f'; # flag for forward/reverse order? nb: ignored for IP
[298]1017 my $sep = shift || '-'; # Separator character - unlikely we'll ever need more than . or -
[265]1018
[298]1019 my $prefix;
[265]1020
1021 $string =~ s/,/./ if !$zone->{isv6};
1022 $string =~ s/,/::/ if $zone->{isv6};
1023
[298]1024 # Subbing ZONE in the host. We need to properly ID the netblock range
1025 # The subbed text should have "network IP with trailing zeros stripped" for
1026 # blocks lined up on octet (for v4) or hex-quad (for v6) boundaries
1027 # For blocks that do NOT line up on these boundaries, we take the most
1028 # significant octet or 16-bit chunk of the "broadcast" IP and append it
1029 # after a double-dash
1030 # ie:
1031 # 8.0.0.0/6 -> 8.0.0.0 -> 11.255.255.255; sub should be 8--11
1032 # 10.0.0.0/12 -> 10.0.0.0 -> 10.0.0.0 -> 10.15.255.255; sub should be 10-0--15
1033 # 192.168.4.0/22 -> 192.168.4.0 -> 192.168.7.255; sub should be 192-168-4--7
1034 # 192.168.0.8/29 -> 192.168.0.8 -> 192.168.0.15; sub should be 192-168-0-8--15
1035 # Similar for v6
[265]1036
[298]1037 if (!$zone->{isv6}) { # IPv4
1038
1039 $prefix = $zone->network->addr; # Just In Case someone managed to slip in
1040 # a funky subnet that had host bits set.
1041 my $bc = $zone->broadcast->addr;
1042
1043 if ($zone->masklen > 24) {
1044 $bc =~ s/^\d+\.\d+\.\d+\.//;
1045 } elsif ($zone->masklen > 16) {
1046 $prefix =~ s/\.0$//;
1047 $bc =~ s/^\d+\.\d+\.//;
1048 } elsif ($zone->masklen > 8) {
1049 $bc =~ s/^\d+\.//;
1050 $prefix =~ s/\.0\.0$//;
1051 } else {
1052 $prefix =~ s/\.0\.0\.0$//;
1053 }
1054 if ($zone->masklen % 8) {
1055 $bc =~ s/(\.255)+$//;
1056 $prefix .= "--$bc"; #"--".zone->masklen; # use range or mask length?
1057 }
1058 if ($fr eq 'f') {
1059 $prefix =~ s/\.+/$sep/g;
1060 } else {
1061 $prefix = join($sep, reverse(split(/\./, $prefix)));
1062 }
1063
1064 } else { # IPv6
1065
1066 if ($fr eq 'f') {
1067
1068 $prefix = $zone->network->addr; # Just In Case someone managed to slip in
1069 # a funky subnet that had host bits set.
[265]1070 my $bc = $zone->broadcast->addr;
1071 if (($zone->masklen % 16) != 0) {
[298]1072 # Strip trailing :0 off $prefix, and :ffff off the broadcast IP
1073 for (my $i=0; $i<(7-int($zone->masklen / 16)); $i++) {
1074 $prefix =~ s/:0$//;
1075 $bc =~ s/:ffff$//;
1076 }
1077 # Strip the leading 16-bit chunks off the front of the broadcast IP
1078 $bc =~ s/^([a-f0-9]+:)+//;
1079 # Append the remaining 16-bit chunk to the prefix after "--"
1080 $prefix .= "--$bc";
[265]1081 } else {
[298]1082 # Strip off :0 from the end until we reach the netblock length.
1083 for (my $i=0; $i<(8-$zone->masklen / 16); $i++) {
[265]1084 $prefix =~ s/:0$//;
[298]1085 }
[265]1086 }
[298]1087 # Actually deal with the separator
1088 $prefix =~ s/:/$sep/g;
1089
1090 } else { # $fr eq 'f'
1091
1092 $prefix = $zone->network->full; # Just In Case someone managed to slip in
1093 # a funky subnet that had host bits set.
1094 my $bc = $zone->broadcast->full;
1095 $prefix =~ s/://g; # clean these out since they're not spaced right for this case
1096 $bc =~ s/://g;
1097 # Strip trailing 0 off $prefix, and f off the broadcast IP, to match the mask length
1098 for (my $i=0; $i<(31-int($zone->masklen / 4)); $i++) {
1099 $prefix =~ s/0$//;
1100 $bc =~ s/f$//;
1101 }
1102 # Split and reverse the order of the nibbles in the network/broadcast IPs
[332]1103 # trim another 0 for nibble-aligned blocks first, but only if we really have a block, not an IP
1104 $prefix =~ s/0$// if $zone->masklen % 4 == 0 && $zone->masklen != 128;
[298]1105 my @nbits = reverse split //, $prefix;
1106 my @bbits = reverse split //, $bc;
1107 # Handle the sub-nibble case. Eww. I feel dirty supporting this...
1108 $nbits[0] = "$nbits[0]-$bbits[0]" if ($zone->masklen % 4) != 0;
1109 # Glue it back together
1110 $prefix = join($sep, @nbits);
1111
1112 } # $fr ne 'f'
1113
1114 } # $zone->{isv6}
1115
1116 # Do the substitution, finally
1117 $string =~ s/ZONE/$prefix/;
1118 $string =~ s/--/-/ if $sep ne '-'; # - as separator needs extra help for sub-octet v4 netblocks
1119 return $string;
1120} # done _ZONE()
1121
1122# Not quite a substitution sub, but placed here as it's basically the inverse of above;
1123# given the .arpa zone name, return the CIDR netblock the zone is for.
[304]1124# Supports v4 non-octet/non-classful netblocks as per the method outlined in the Grasshopper Book (2nd Ed p217-218)
[298]1125# Does NOT support non-quad v6 netblocks via the same scheme; it shouldn't ever be necessary.
1126# Takes a nominal .arpa zone name, returns a success code and NetAddr::IP, or a fail code and message
1127sub _zone2cidr {
1128 my $zone = shift;
1129
1130 my $cidr;
[301]1131 my $tmpcidr;
1132 my $warnmsg = '';
[298]1133
1134 if ($zone =~ /\.in-addr\.arpa\.?$/) {
1135 # v4 revzone, formal zone name type
1136 my $tmpzone = $zone;
1137 $tmpzone =~ s/\.in-addr\.arpa\.?//;
[301]1138 return ('FAIL', "Non-numerics in apparent IPv4 reverse zone name") if $tmpzone !~ /^(?:\d+-)?[\d\.]+$/;
[298]1139
1140 # Snag the octet pieces
1141 my @octs = split /\./, $tmpzone;
1142
1143 # Map result of a range manipulation to a mask length change. Cheaper than finding the 2-root of $octets[0]+1.
[304]1144 # Note we will not support /31 blocks, mostly due to issues telling "24-31" -> .24/29 apart from
1145 # "24-31" -> .24/31", with a litte bit of "/31 is icky".
1146 my %maskmap = ( 3 => 2, 7 => 3, 15 => 4, 31 => 5, 63 => 6, 127 => 7,
1147 30 => 2, 29 => 3, 28 => 4, 27 => 5, 26 => 6, 25 => 7
1148 );
[298]1149
1150 # Handle "range" blocks, eg, 80-83.168.192.in-addr.arpa (192.168.80.0/22)
1151 # Need to take the size of the range to offset the basic octet-based mask length,
1152 # and make sure the first number in the range gets used as the network address for the block
[304]1153 # Alternate form: The second number is actually the real netmask, not the end of the range.
[298]1154 my $masklen = 0;
[307]1155 if ($octs[0] =~ /^((\d+)-(\d+))$/) { # take the range...
1156 if (24 < $3 && $3 < 31) {
[304]1157 # we have a real netmask
[307]1158 $masklen = -$maskmap{$3};
[304]1159 } else {
1160 # we have a range. NB: only real CIDR ranges are supported
[307]1161 $masklen -= $maskmap{-(eval $1)}; # find the mask base...
[304]1162 }
[307]1163 $octs[0] = $2; # set the base octet of the range...
[265]1164 }
[298]1165 @octs = reverse @octs; # We can reverse the octet pieces now that we've extracted and munged any ranges
[265]1166
[304]1167# arguably we should only allow sub-octet range/mask in-addr.arpa
1168# specifications in the least significant octet, but the code is
1169# simpler if we deal with sub-octet delegations at any level.
1170
[298]1171 # Now we find the "true" mask with the aid of the "base" calculated above
1172 if ($#octs == 0) {
1173 $masklen += 8;
1174 $tmpcidr = "$octs[0].0.0.0/$masklen"; # really hope we don't see one of these very often.
1175 } elsif ($#octs == 1) {
1176 $masklen += 16;
1177 $tmpcidr = "$octs[0].$octs[1].0.0/$masklen";
1178 } elsif ($#octs == 2) {
1179 $masklen += 24;
1180 $tmpcidr = "$octs[0].$octs[1].$octs[2].0/$masklen";
[265]1181 } else {
[298]1182 $masklen += 32;
1183 $tmpcidr = "$octs[0].$octs[1].$octs[2].$octs[3]/$masklen";
[265]1184 }
1185
[298]1186 } elsif ($zone =~ /\.ip6\.arpa$/) {
1187 # v6 revzone, formal zone name type
1188 my $tmpzone = $zone;
1189 $tmpzone =~ s/\.ip6\.arpa\.?//;
[301]1190##fixme: if-n-when we decide we can support sub-nibble v6 zone names, we'll need to change this segment
1191 return ('FAIL', "Non-hexadecimals in apparent IPv6 reverse zone name") if $tmpzone !~ /^[a-fA-F\d\.]+$/;
[298]1192 my @quads = reverse(split(/\./, $tmpzone));
1193 $warnmsg .= "Apparent sub-/64 IPv6 reverse zone\n" if $#quads > 15;
1194 my $nc;
1195 foreach (@quads) {
[301]1196 $tmpcidr .= $_;
1197 $tmpcidr .= ":" if ++$nc % 4 == 0;
[298]1198 }
1199 my $nq = 1 if $nc % 4 != 0;
1200 my $mask = $nc * 4; # need to do this here because we probably increment it below
1201 while ($nc++ % 4 != 0) {
[301]1202 $tmpcidr .= "0";
[298]1203 }
[301]1204 $tmpcidr .= ($nq ? '::' : ':')."/$mask";
[298]1205 }
[301]1206
1207 # Just to be sure, use NetAddr::IP to validate. Saves a lot of nasty regex watching for valid octet values.
1208 return ('FAIL', "Invalid zone $zone (apparent netblock $tmpcidr)")
1209 unless $cidr = NetAddr::IP->new($tmpcidr);
1210
1211 if ($warnmsg) {
1212 $errstr = $warnmsg;
1213 return ('WARN', $cidr);
1214 }
1215 return ('OK', $cidr);
[298]1216} # done _zone2cidr()
[265]1217
[337]1218# Record template %-parameter expansion, IPv4. Note that IPv6 doesn't
1219# really have a sane way to handle this type of expansion at the moment
1220# due to the size of the address space.
1221# Takes a reference to a template string to be expanded, and an IP to use in the replacement.
1222sub _template4_expand {
1223 my $tmpl = shift;
1224 my $ip = shift;
[298]1225
[337]1226 my @ipparts = split /\./, $ip;
1227 my @iphex;
1228 my @ippad;
1229 for (@ipparts) {
1230 push @iphex, sprintf("%x", $_);
[443]1231 push @ippad, sprintf("%0.3u", $_);
[337]1232 }
1233
1234 # IP substitutions in template records:
1235 #major patterns:
1236 #dashed IP, forward and reverse
[532]1237 #underscoreed IP, forward and reverse
[337]1238 #dotted IP, forward and reverse (even if forward is... dumb)
[532]1239 # -> %r for reverse, %i for forward, leading -, _, or . to indicate separator, defaults to -
[337]1240 # %r or %-r => %4d-%3d-%2d-%1d
[532]1241 # %_r => %4d_%3d_%2d_%1d
[337]1242 # %.r => %4d.%3d.%2d.%1d
1243 # %i or %-i => %1d-%2d-%3d-%4d
[532]1244 # %_i => %1d_%2d_%3d_%4d
[337]1245 # %.i => %1d.%2d.%3d.%4d
1246 $$tmpl =~ s/\%r/\%4d-\%3d-\%2d-\%1d/g;
[532]1247 $$tmpl =~ s/\%([-._])r/\%4d$1\%3d$1\%2d$1\%1d/g;
[337]1248 $$tmpl =~ s/\%i/\%1d-\%2d-\%3d-\%4d/g;
[532]1249 $$tmpl =~ s/\%([-._])i/\%1d$1\%2d$1\%3d$1\%4d/g;
[337]1250
1251 #hex-coded IP
1252 # %h
1253 $$tmpl =~ s/\%h/$iphex[0]$iphex[1]$iphex[2]$iphex[3]/g;
1254
1255 #IP as decimal-coded 32-bit value
1256 # %d
1257 my $iptmp = $ipparts[0]*256*256*256 + $ipparts[1]*256*256 + $ipparts[2]*256 + $ipparts[3];
1258 $$tmpl =~ s/\%d/$iptmp/g;
1259
1260 #minor patterns (per-octet)
1261 # %[1234][dh0]
1262 #octet
1263 #hex-coded octet
1264 #0-padded octet
1265 $$tmpl =~ s/\%([1234])d/$ipparts[$1-1]/g;
1266 $$tmpl =~ s/\%([1234])h/$iphex[$1-1]/g;
[443]1267 $$tmpl =~ s/\%([1234])0/$ippad[$1-1]/g;
[337]1268} # _template4_expand()
1269
[532]1270# Broad syntactic check on the hostname. Checks for valid characters, correctly-expandable template patterns.
1271# Takes the hostname, type, and live/default and forward/reverse flags
1272# Returns true/false, sets errstr on failures
1273sub _check_hostname_form {
1274 my ($hname,$rectype,$defrec,$revrec) = @_;
[337]1275
[532]1276 if ($hname =~ /\%/ && ($rectype == 65282 || $rectype == 65283) ) {
1277 my $tmphost = $hname;
1278 # we don't actually need to test with the real IP passed; that saves a bit of fiddling.
1279 _template4_expand(\$tmphost, '10.10.10.10');
1280 if ($tmphost =~ /\%/) {
1281 $errstr = "Invalid template $hname";
1282 return;
1283 }
1284 } elsif ($revrec eq 'y') {
1285 # Reverse zones don't support @ in hostnames
1286 # Also skip failure on revzone TXT records; the hostname contains the TXT content in that case.
1287 if ($rectype != $reverse_typemap{TXT} && lc($hname) !~ /^[0-9a-z_.-]+$/) {
1288 $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
1289 return;
1290 }
1291 } else {
1292 if (lc($hname) !~ /^(?:[0-9a-z_.-]+|@)$/) {
1293 # Don't mention @, because it would be far too wordy to explain the nuance of @
1294 $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
1295 return;
1296 }
1297 }
1298 return 1;
1299} # _check_hostname_form()
1300
1301
[228]1302##
[2]1303## Initialization and cleanup subs
1304##
1305
[128]1306## DNSDB::__cfgload()
1307# Private sub to parse a config file and load it into %config
[517]1308# Takes a filename and a hashref to put the parsed entries in
[128]1309sub __cfgload {
1310 $errstr = '';
1311 my $cfgfile = shift;
[517]1312 my $cfg = shift;
[131]1313
[128]1314 if (open CFG, "<$cfgfile") {
1315 while (<CFG>) {
1316 chomp;
1317 s/^\s*//;
1318 next if /^#/;
1319 next if /^$/;
1320# hmm. more complex bits in this file might require [heading] headers, maybe?
1321# $mode = $1 if /^\[(a-z)+]/;
1322 # DB connect info
[517]1323 $cfg->{dbname} = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i;
1324 $cfg->{dbuser} = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i;
1325 $cfg->{dbpass} = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i;
1326 $cfg->{dbhost} = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i;
[128]1327 # Mail settings
[517]1328 $cfg->{mailhost} = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i;
1329 $cfg->{mailnotify} = $1 if /^mailnotify\s*=\s*([a-z0-9_.\@-]+)/i;
1330 $cfg->{mailsender} = $1 if /^mailsender\s*=\s*([a-z0-9_.\@-]+)/i;
1331 $cfg->{mailname} = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i;
1332 $cfg->{orgname} = $1 if /^orgname\s*=\s*([a-z0-9\s_.,'-]+)/i;
1333 $cfg->{domain} = $1 if /^domain\s*=\s*([a-z0-9_.-]+)/i;
[163]1334 # session - note this is fed directly to CGI::Session
[517]1335 $cfg->{timeout} = $1 if /^[tT][iI][mM][eE][oO][uU][tT]\s*=\s*(\d+[smhdwMy]?)/;
1336 $cfg->{sessiondir} = $1 if m{^sessiondir\s*=\s*([a-z0-9/_.-]+)}i;
[201]1337 # misc
[517]1338 $cfg->{log_failures} = $1 if /^log_failures\s*=\s*([a-z01]+)/i;
1339 $cfg->{perpage} = $1 if /^perpage\s*=\s*(\d+)/i;
1340 $cfg->{exportcache} = $1 if m{^exportcache\s*=\s*([a-z0-9/_.-]+)}i;
[559]1341 $cfg->{usecache} = $1 if m{^usecache\s*=\s*([a-z01]+)}i;
1342 $cfg->{force_refresh} = $1 if /^force_refresh\s*=\s*([a-z01]+)/i;
[542]1343 $cfg->{lowercase} = $1 if /^lowercase\s*=\s*([a-z01]+)/i;
[517]1344# not supported in dns.cgi yet
1345# $cfg->{templatedir} = $1 if m{^templatedir\s*=\s*([a-z0-9/_.-]+)}i;
1346# $cfg->{templateoverride} = $1 if m{^templateoverride\s*=\s*([a-z0-9/_.-]+)}i;
[400]1347 # RPC options
[517]1348 $cfg->{rpcmode} = $1 if /^rpc_mode\s*=\s*(socket|HTTP|XMLRPC)\s*$/i;
1349 $cfg->{maxfcgi} = $1 if /^max_fcgi_requests\s*=\s*(\d+)\s*$/i;
[465]1350 if (my ($tmp) = /^rpc_iplist\s*=\s*(.+)/i) {
1351 my @ips = split /[,\s]+/, $tmp;
1352 my $rpcsys = shift @ips;
[517]1353 push @{$cfg->{rpcacl}{$rpcsys}}, @ips;
[400]1354 }
[128]1355 }
1356 close CFG;
1357 } else {
[517]1358 $errstr = "Couldn't load configuration file $cfgfile: $!";
[128]1359 return;
1360 }
1361 return 1;
1362} # end __cfgload()
1363
1364
[2]1365## DNSDB::connectDB()
1366# Creates connection to DNS database.
1367# Requires the database name, username, and password.
[465]1368# Returns a handle to the db or undef on failure.
[2]1369# Set up for a PostgreSQL db; could be any transactional DBMS with the
1370# right changes.
[465]1371# Called by new(); not intended to be called publicly.
[2]1372sub connectDB {
1373 $errstr = '';
[15]1374 my $dbname = shift;
1375 my $user = shift;
1376 my $pass = shift;
[2]1377 my $dbh;
1378 my $DSN = "DBI:Pg:dbname=$dbname";
1379
1380 my $host = shift;
1381 $DSN .= ";host=$host" if $host;
1382
1383# Note that we want to autocommit by default, and we will turn it off locally as necessary.
1384# We may not want to print gobbledygook errors; YMMV. Have to ponder that further.
1385 $dbh = DBI->connect($DSN, $user, $pass, {
1386 AutoCommit => 1,
1387 PrintError => 0
[465]1388 });
1389 if (!$dbh) {
1390 $errstr = $DBI::errstr;
1391 return;
1392 }
1393#) if(!$dbh);
[2]1394
[465]1395 local $dbh->{RaiseError} = 1;
1396
1397 eval {
[212]1398##fixme: initialize the DB if we can't find the table (since, by definition, there's
1399# nothing there if we can't select from it...)
[465]1400 my $tblsth = $dbh->prepare("SELECT count(*) FROM pg_catalog.pg_class WHERE relkind='r' AND relname=?");
1401 my ($tblcount) = $dbh->selectrow_array($tblsth, undef, ('misc'));
1402# return (undef,$DBI::errstr) if $dbh->err;
[212]1403
1404#if ($tblcount == 0) {
1405# # create tables one at a time, checking for each.
1406# return (undef, "check table misc missing");
1407#}
1408
1409# Return here if we can't select.
1410# This should retrieve the dbversion key.
[465]1411 my $sth = $dbh->prepare("SELECT key,value FROM misc WHERE misc_id=1");
1412 $sth->execute();
1413# return (undef,$DBI::errstr) if ($sth->err);
[2]1414
[212]1415##fixme: do stuff to the DB on version mismatch
1416# x.y series should upgrade on $DNSDB::VERSION > misc(key=>version)
1417# DB should be downward-compatible; column defaults should give sane (if possibly
1418# useless-and-needs-help) values in columns an older software stack doesn't know about.
1419
[2]1420# See if the select returned anything (or null data). This should
1421# succeed if the select executed, but...
[465]1422 $sth->fetchrow();
1423# return (undef,$DBI::errstr) if ($sth->err);
[2]1424
[465]1425 $sth->finish;
[2]1426
[465]1427 }; # wrapped DB checks
1428 if ($@) {
1429 $errstr = $@;
1430 return;
1431 }
1432
[2]1433# If we get here, we should be OK.
[465]1434 return $dbh;
[2]1435} # end connectDB
1436
1437
1438## DNSDB::finish()
1439# Cleans up after database handles and so on.
1440# Requires a database handle
1441sub finish {
[465]1442 my $self = shift;
1443 $self->{dbh}->disconnect;
[2]1444} # end finish
1445
1446
1447## DNSDB::initGlobals()
1448# Initialize global variables
1449# NB: this does NOT include web-specific session variables!
1450sub initGlobals {
[465]1451 my $self = shift;
1452 my $dbh = $self->{dbh};
[2]1453
[208]1454# load record types from database
[228]1455 my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes");
[2]1456 $sth->execute;
[228]1457 while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) {
[2]1458 $typemap{$recval} = $recname;
1459 $reverse_typemap{$recname} = $recval;
[228]1460 # now we fill the record validation function hash
1461 if ($stdflag < 5) {
1462 my $fn = "_validate_$recval";
1463 $validators{$recval} = \&$fn;
1464 } else {
1465 my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }";
1466 $validators{$recval} = eval $fn;
1467 }
[2]1468 }
1469} # end initGlobals
1470
1471
[401]1472## DNSDB::initRPC()
[469]1473# Takes a remote username and remote fullname.
[401]1474# Sets up the RPC logging-pseudouser if needed.
1475# Sets the %userdata hash for logging.
1476# Returns undef on failure
1477sub initRPC {
[469]1478 my $self = shift;
1479 my $dbh = $self->{dbh};
[401]1480 my %args = @_;
1481
1482 return if !$args{username};
1483 return if !$args{fullname};
1484
[406]1485 $args{username} = "$args{username}/$args{rpcsys}";
1486
1487 my $tmpuser = $dbh->selectrow_hashref("SELECT username,user_id AS userid,group_id,firstname,lastname,status".
[404]1488 " FROM users WHERE username=?", undef, ($args{username}) );
[406]1489 if (!$tmpuser) {
[401]1490 $dbh->do("INSERT INTO users (username,password,firstname,type) VALUES (?,'RPC',?,'R')", undef,
1491 ($args{username}, $args{fullname}) );
[406]1492 $tmpuser = $dbh->selectrow_hashref("SELECT username,user_id AS userid,group_id,firstname,lastname,status".
[404]1493 " FROM users WHERE username=?", undef, ($args{username}) );
[401]1494 }
[488]1495 $tmpuser->{lastname} = '' if !$tmpuser->{lastname};
1496 $self->{loguserid} = $tmpuser->{userid};
1497 $self->{logusername} = $tmpuser->{username};
1498 $self->{logfullname} = "$tmpuser->{firstname} $tmpuser->{lastname} ($args{rpcsys})";
[406]1499 return 1 if $tmpuser;
[401]1500} # end initRPC()
1501
1502
[278]1503## DNSDB::login()
1504# Takes a database handle, username and password
[316]1505# Returns a userdata hash (UID, GID, username, fullname parts) if username exists,
1506# password matches the one on file, and account is not disabled
[278]1507# Returns undef otherwise
1508sub login {
[469]1509 my $self = shift;
1510 my $dbh = $self->{dbh};
[278]1511 my $user = shift;
1512 my $pass = shift;
1513
[316]1514 my $userinfo = $dbh->selectrow_hashref("SELECT user_id,group_id,password,firstname,lastname,status".
1515 " FROM users WHERE username=?",
[279]1516 undef, ($user) );
1517 return if !$userinfo;
[316]1518 return if !$userinfo->{status};
[278]1519
[279]1520 if ($userinfo->{password} =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) {
[278]1521 # native passwords (crypt-md5)
[279]1522 return if $userinfo->{password} ne unix_md5_crypt($pass,$1);
1523 } elsif ($userinfo->{password} =~ /^[0-9a-f]{32}$/) {
[278]1524 # VegaDNS import (hex-coded MD5)
[279]1525 return if $userinfo->{password} ne md5_hex($pass);
[278]1526 } else {
1527 # plaintext (convenient now and then)
[279]1528 return if $userinfo->{password} ne $pass;
[278]1529 }
1530
[279]1531 return $userinfo;
[278]1532} # end login()
1533
1534
[279]1535## DNSDB::initActionLog()
1536# Set up action logging. Takes a database handle and user ID
1537# Sets some internal globals and Does The Right Thing to set up a logging channel.
1538# This sets up _log() to spew out log entries to the defined channel without worrying
1539# about having to open a file or a syslog channel
1540##fixme Need to call _initActionLog_blah() for various logging channels, configured
[517]1541# via dnsdb.conf, in $self->{log_channel} or something
[279]1542# See https://secure.deepnet.cx/trac/dnsadmin/ticket/21
1543sub initActionLog {
[469]1544 my $self = shift;
1545 my $dbh = $self->{dbh};
[279]1546 my $uid = shift;
1547
1548 return if !$uid;
1549
1550 # snag user info for logging. there's got to be a way to not have to pass this back
1551 # and forth from a caller, but web usage means no persistence we can rely on from
1552 # the server side.
1553 my ($username,$fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname".
1554 " FROM users WHERE user_id=?", undef, ($uid));
1555##fixme: errors are unpossible!
1556
[488]1557 $self->{logusername} = $username;
1558 $self->{loguserid} = $uid;
1559 $self->{logfullname} = $fullname;
[279]1560
1561 # convert to real check once we have other logging channels
[517]1562 # if ($self->{log_channel} eq 'sql') {
[279]1563 # Open Log, Sez Me!
1564 # }
1565
1566} # end initActionLog
1567
1568
[65]1569## DNSDB::getPermissions()
1570# Get permissions from DB
1571# Requires DB handle, group or user flag, ID, and hashref.
1572sub getPermissions {
[469]1573 my $self = shift;
1574 my $dbh = $self->{dbh};
1575
[65]1576 my $type = shift;
1577 my $id = shift;
1578 my $hash = shift;
1579
1580 my $sql = qq(
1581 SELECT
1582 p.admin,p.self_edit,
1583 p.group_create,p.group_edit,p.group_delete,
1584 p.user_create,p.user_edit,p.user_delete,
1585 p.domain_create,p.domain_edit,p.domain_delete,
[387]1586 p.record_create,p.record_edit,p.record_delete,p.record_locchg,
[382]1587 p.location_create,p.location_edit,p.location_delete,p.location_view
[65]1588 FROM permissions p
1589 );
1590 if ($type eq 'group') {
1591 $sql .= qq(
1592 JOIN groups g ON g.permission_id=p.permission_id
1593 WHERE g.group_id=?
1594 );
1595 } else {
1596 $sql .= qq(
1597 JOIN users u ON u.permission_id=p.permission_id
1598 WHERE u.user_id=?
1599 );
1600 }
1601
1602 my $sth = $dbh->prepare($sql);
1603
[514]1604##fixme? we don't trap other plain SELECT errors
1605 $sth->execute($id);
[65]1606
1607# my $permref = $sth->fetchrow_hashref;
1608# return $permref;
1609# $hash = $permref;
1610# Eww. Need to learn how to forcibly drop a hashref onto an existing hash.
1611 ($hash->{admin},$hash->{self_edit},
1612 $hash->{group_create},$hash->{group_edit},$hash->{group_delete},
1613 $hash->{user_create},$hash->{user_edit},$hash->{user_delete},
1614 $hash->{domain_create},$hash->{domain_edit},$hash->{domain_delete},
[387]1615 $hash->{record_create},$hash->{record_edit},$hash->{record_delete},$hash->{record_locchg},
[382]1616 $hash->{location_create},$hash->{location_edit},$hash->{location_delete},$hash->{location_view}
1617 ) = $sth->fetchrow_array;
[65]1618
1619} # end getPermissions()
1620
1621
1622## DNSDB::changePermissions()
1623# Update an ACL entry
1624# Takes a db handle, type, owner-id, and hashref for the changed permissions.
1625sub changePermissions {
[474]1626 my $self = shift;
1627 my $dbh = $self->{dbh};
[65]1628 my $type = shift;
1629 my $id = shift;
1630 my $newperms = shift;
[87]1631 my $inherit = shift || 0;
[65]1632
[294]1633 my $resultmsg = '';
[66]1634
[87]1635 # see if we're switching from inherited to custom. for bonus points,
1636 # snag the permid and parent permid anyway, since we'll need the permid
1637 # to set/alter custom perms, and both if we're switching from custom to
1638 # inherited.
[294]1639 my $sth = $dbh->prepare("SELECT (u.permission_id=g.permission_id) AS was_inherited,u.permission_id,g.permission_id,".
1640 ($type eq 'user' ? 'u.group_id,u.username' : 'u.parent_group_id,u.group_name').
[65]1641 " FROM ".($type eq 'user' ? 'users' : 'groups')." u ".
[66]1642 " JOIN groups g ON u.".($type eq 'user' ? '' : 'parent_')."group_id=g.group_id ".
[65]1643 " WHERE u.".($type eq 'user' ? 'user' : 'group')."_id=?");
1644 $sth->execute($id);
1645
[294]1646 my ($wasinherited,$permid,$parpermid,$parid,$name) = $sth->fetchrow_array;
[66]1647
[78]1648# hack phtoui
1649# group id 1 is "special" in that it's it's own parent (err... possibly.)
1650# may make its parent id 0 which doesn't exist, and as a bonus is Perl-false.
1651 $wasinherited = 0 if ($type eq 'group' && $id == 1);
1652
[66]1653 local $dbh->{AutoCommit} = 0;
1654 local $dbh->{RaiseError} = 1;
1655
1656 # Wrap all the SQL in a transaction
1657 eval {
[87]1658 if ($inherit) {
1659
1660 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='t',permission_id=? ".
1661 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($parpermid, $id) );
1662 $dbh->do("DELETE FROM permissions WHERE permission_id=?", undef, ($permid) );
1663
1664 } else {
1665
1666 if ($wasinherited) { # munge new permission entry in if we're switching from inherited perms
[66]1667##fixme: need to add semirecursive bit to properly munge inherited permission ID on subgroups and users
[87]1668# ... if'n'when we have groups with fully inherited permissions.
1669 # SQL is coo
1670 $dbh->do("INSERT INTO permissions ($permlist,".($type eq 'user' ? 'user' : 'group')."_id) ".
1671 "SELECT $permlist,? FROM permissions WHERE permission_id=?", undef, ($id,$permid) );
1672 ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions ".
1673 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($id) );
1674 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='f',permission_id=? ".
1675 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($permid, $id) );
[66]1676 }
[78]1677
[87]1678 # and now set the permissions we were passed
1679 foreach (@permtypes) {
1680 if (defined ($newperms->{$_})) {
1681 $dbh->do("UPDATE permissions SET $_=? WHERE permission_id=?", undef, ($newperms->{$_},$permid) );
1682 }
1683 }
1684
1685 } # (inherited->)? custom
1686
[294]1687 if ($type eq 'user') {
1688 $resultmsg = "Updated permissions for user $name";
1689 } else {
1690 $resultmsg = "Updated default permissions for group $name";
1691 }
[487]1692 $self->_log(group_id => ($type eq 'user' ? $parid : $id), entry => $resultmsg);
[66]1693 $dbh->commit;
1694 }; # end eval
1695 if ($@) {
1696 my $msg = $@;
1697 eval { $dbh->rollback; };
[294]1698 return ('FAIL',"Error changing permissions: $msg");
[66]1699 }
1700
[294]1701 return ('OK',$resultmsg);
[65]1702} # end changePermissions()
1703
1704
[67]1705## DNSDB::comparePermissions()
1706# Compare two permission hashes
1707# Returns '>', '<', '=', '!'
1708sub comparePermissions {
1709 my $p1 = shift;
1710 my $p2 = shift;
1711
1712 my $retval = '='; # assume equality until proven otherwise
1713
1714 no warnings "uninitialized";
1715
1716 foreach (@permtypes) {
1717 next if $p1->{$_} == $p2->{$_}; # equal is good
1718 if ($p1->{$_} && !$p2->{$_}) {
1719 if ($retval eq '<') { # if we've already found an unequal pair where
1720 $retval = '!'; # $p2 has more access, and we now find a pair
1721 last; # where $p1 has more access, the overall access
1722 } # is neither greater or lesser, it's unequal.
1723 $retval = '>';
1724 }
1725 if (!$p1->{$_} && $p2->{$_}) {
1726 if ($retval eq '>') { # if we've already found an unequal pair where
1727 $retval = '!'; # $p1 has more access, and we now find a pair
1728 last; # where $p2 has more access, the overall access
1729 } # is neither greater or lesser, it's unequal.
1730 $retval = '<';
1731 }
1732 }
1733 return $retval;
1734} # end comparePermissions()
1735
1736
[112]1737## DNSDB::changeGroup()
1738# Change group ID of an entity
1739# Takes a database handle, entity type, entity ID, and new group ID
1740sub changeGroup {
[476]1741 my $self = shift;
1742 my $dbh = $self->{dbh};
[112]1743 my $type = shift;
1744 my $id = shift;
1745 my $newgrp = shift;
1746
1747##fixme: fail on not enough args
1748 #return ('FAIL', "Missing
1749
[295]1750 return ('FAIL', "Can't change the group of a $type")
1751 unless grep /^$type$/, ('domain','revzone','user','group'); # could be extended for defrecs?
1752
1753 # Collect some names for logging and messages
1754 my $entname;
[112]1755 if ($type eq 'domain') {
[473]1756 $entname = $self->domainName($id);
[295]1757 } elsif ($type eq 'revzone') {
[473]1758 $entname = $self->revName($id);
[112]1759 } elsif ($type eq 'user') {
[473]1760 $entname = $self->userFullName($id, '%u');
[112]1761 } elsif ($type eq 'group') {
[473]1762 $entname = $self->groupName($id);
[112]1763 }
[295]1764
1765 my ($oldgid) = $dbh->selectrow_array("SELECT group_id FROM $par_tbl{$type} WHERE $id_col{$type}=?",
1766 undef, ($id));
[473]1767 my $oldgname = $self->groupName($oldgid);
1768 my $newgname = $self->groupName($newgrp);
[295]1769
1770 return ('FAIL', "Can't move things into a group that doesn't exist") if !$newgname;
1771
1772 return ('WARN', "Nothing to do, new group is the same as the old group") if $oldgid == $newgrp;
1773
1774 # Allow transactions, and raise an exception on errors so we can catch it later.
1775 # Use local to make sure these get "reset" properly on exiting this block
1776 local $dbh->{AutoCommit} = 0;
1777 local $dbh->{RaiseError} = 1;
1778
1779 eval {
1780 $dbh->do("UPDATE $par_tbl{$type} SET group_id=? WHERE $id_col{$type}=?", undef, ($newgrp, $id));
1781 # Log the change in both the old and new groups
[487]1782 $self->_log(group_id => $oldgid, entry => "Moved $type $entname from $oldgname to $newgname");
1783 $self->_log(group_id => $newgrp, entry => "Moved $type $entname from $oldgname to $newgname");
[295]1784 $dbh->commit;
1785 };
1786 if ($@) {
1787 my $msg = $@;
1788 eval { $dbh->rollback; };
[517]1789 if ($self->{log_failures}) {
[487]1790 $self->_log(group_id => $oldgid, entry => "Error moving $type $entname to $newgname: $msg");
[295]1791 $dbh->commit; # since we enabled transactions earlier
1792 }
1793 return ('FAIL',"Error moving $type $entname to $newgname: $msg");
1794 }
1795
1796 return ('OK',"Moved $type $entname from $oldgname to $newgname");
[112]1797} # end changeGroup()
1798
1799
[2]1800##
1801## Processing subs
1802##
1803
1804## DNSDB::addDomain()
1805# Add a domain
[190]1806# Takes a database handle, domain name, numeric group, boolean(ish) state (active/inactive),
1807# and user info hash (for logging).
[2]1808# Returns a status code and message
1809sub addDomain {
1810 $errstr = '';
[477]1811 my $self = shift;
1812 my $dbh = $self->{dbh};
[2]1813 my $domain = shift;
[497]1814 return ('FAIL',"Domain must not be blank\n") if !$domain;
[2]1815 my $group = shift;
[497]1816 return ('FAIL',"Group must be specified\n") if !defined($group);
[2]1817 my $state = shift;
[497]1818 return ('FAIL',"Domain status must be specified\n") if !defined($state);
[516]1819 my $defloc = shift || '';
[2]1820
[116]1821 $state = 1 if $state =~ /^active$/;
1822 $state = 1 if $state =~ /^on$/;
1823 $state = 0 if $state =~ /^inactive$/;
1824 $state = 0 if $state =~ /^off$/;
1825
1826 return ('FAIL',"Invalid domain status") if $state !~ /^\d+$/;
1827
[542]1828 $domain = lc($domain) if $self->{lowercase};
1829
[190]1830 return ('FAIL', "Invalid characters in domain") if $domain !~ /^[a-zA-Z0-9_.-]+$/;
1831
[349]1832 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)");
[3]1833 my $dom_id;
1834
[38]1835# quick check to start to see if we've already got one
1836 $sth->execute($domain);
1837 ($dom_id) = $sth->fetchrow_array;
1838
1839 return ('FAIL', "Domain already exists") if $dom_id;
1840
[2]1841 # Allow transactions, and raise an exception on errors so we can catch it later.
1842 # Use local to make sure these get "reset" properly on exiting this block
1843 local $dbh->{AutoCommit} = 0;
1844 local $dbh->{RaiseError} = 1;
1845
1846 # Wrap all the SQL in a transaction
1847 eval {
1848 # insert the domain...
[516]1849 $dbh->do("INSERT INTO domains (domain,group_id,status,default_location) VALUES (?,?,?,?)", undef,
1850 ($domain, $group, $state, $defloc));
[2]1851
1852 # get the ID...
[349]1853 ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)",
1854 undef, ($domain));
[2]1855
[487]1856 $self->_log(domain_id => $dom_id, group_id => $group,
1857 entry => "Added ".($state ? 'active' : 'inactive')." domain $domain");
[190]1858
[2]1859 # ... and now we construct the standard records from the default set. NB: group should be variable.
[190]1860 my $sth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?");
[516]1861 my $sth_in = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,distance,weight,port,ttl,location)".
1862 " VALUES ($dom_id,?,?,?,?,?,?,?,?)");
[190]1863 $sth->execute($group);
[516]1864 while (my ($host, $type, $val, $dist, $weight, $port, $ttl) = $sth->fetchrow_array()) {
[2]1865 $host =~ s/DOMAIN/$domain/g;
[37]1866 $val =~ s/DOMAIN/$domain/g;
[516]1867 $sth_in->execute($host, $type, $val, $dist, $weight, $port, $ttl, $defloc);
[190]1868 if ($typemap{$type} eq 'SOA') {
1869 my @tmp1 = split /:/, $host;
1870 my @tmp2 = split /:/, $val;
[487]1871 $self->_log(domain_id => $dom_id, group_id => $group,
[284]1872 entry => "[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
[487]1873 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl");
[190]1874 } else {
1875 my $logentry = "[new $domain] Added record '$host $typemap{$type}";
1876 $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX';
1877 $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV';
[487]1878 $self->_log(domain_id => $dom_id, group_id => $group,
1879 entry => $logentry." $val', TTL $ttl");
[190]1880 }
[2]1881 }
1882
1883 # once we get here, we should have suceeded.
1884 $dbh->commit;
1885 }; # end eval
1886
1887 if ($@) {
1888 my $msg = $@;
1889 eval { $dbh->rollback; };
[487]1890 $self->_log(group_id => $group, entry => "Failed adding domain $domain ($msg)")
[517]1891 if $self->{log_failures};
[284]1892 $dbh->commit; # since we enabled transactions earlier
[193]1893 return ('FAIL',$msg);
[2]1894 } else {
[3]1895 return ('OK',$dom_id);
[2]1896 }
1897} # end addDomain
1898
1899
[274]1900## DNSDB::delZone()
1901# Delete a forward or reverse zone.
1902# Takes a database handle, zone ID, and forward/reverse flag.
[3]1903# for now, just delete the records, then the domain.
1904# later we may want to archive it in some way instead (status code 2, for example?)
[274]1905sub delZone {
[477]1906 my $self = shift;
1907 my $dbh = $self->{dbh};
[274]1908 my $zoneid = shift;
1909 my $revrec = shift;
[3]1910
1911 # Allow transactions, and raise an exception on errors so we can catch it later.
1912 # Use local to make sure these get "reset" properly on exiting this block
1913 local $dbh->{AutoCommit} = 0;
1914 local $dbh->{RaiseError} = 1;
1915
[285]1916 my $msg = '';
[23]1917 my $failmsg = '';
[473]1918 my $zone = ($revrec eq 'n' ? $self->domainName($zoneid) : $self->revName($zoneid));
[23]1919
[343]1920 return ('FAIL', ($revrec eq 'n' ? 'Domain' : 'Reverse zone')." ID $zoneid doesn't exist") if !$zone;
1921
[517]1922 # Set this up here since we may use if if $self->{log_failures} is enabled
[285]1923 my %loghash;
1924 $loghash{domain_id} = $zoneid if $revrec eq 'n';
1925 $loghash{rdns_id} = $zoneid if $revrec eq 'y';
[473]1926 $loghash{group_id} = $self->parentID(
1927 id => $zoneid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec);
[285]1928
[3]1929 # Wrap all the SQL in a transaction
1930 eval {
[274]1931 # Disentangle custom record types before removing the
1932 # ones that are only in the zone to be deleted
1933 if ($revrec eq 'n') {
1934 my $sth = $dbh->prepare("UPDATE records SET type=?,domain_id=0 WHERE domain_id=? AND type=?");
1935 $failmsg = "Failure converting multizone types to single-zone";
1936 $sth->execute($reverse_typemap{PTR}, $zoneid, 65280);
1937 $sth->execute($reverse_typemap{PTR}, $zoneid, 65281);
1938 $sth->execute(65282, $zoneid, 65283);
1939 $sth->execute(65282, $zoneid, 65284);
1940 $failmsg = "Failure removing domain records";
1941 $dbh->do("DELETE FROM records WHERE domain_id=?", undef, ($zoneid));
1942 $failmsg = "Failure removing domain";
1943 $dbh->do("DELETE FROM domains WHERE domain_id=?", undef, ($zoneid));
1944 } else {
1945 my $sth = $dbh->prepare("UPDATE records SET type=?,rdns_id=0 WHERE rdns_id=? AND type=?");
1946 $failmsg = "Failure converting multizone types to single-zone";
1947 $sth->execute($reverse_typemap{A}, $zoneid, 65280);
1948 $sth->execute($reverse_typemap{AAAA}, $zoneid, 65281);
1949# We don't have an "A template" or "AAAA template" type, although it might be useful for symmetry.
[345]1950# $sth->execute(65286?, $zoneid, 65283);
1951# $sth->execute(65286?, $zoneid, 65284);
[274]1952 $failmsg = "Failure removing reverse records";
1953 $dbh->do("DELETE FROM records WHERE rdns_id=?", undef, ($zoneid));
1954 $failmsg = "Failure removing reverse zone";
1955 $dbh->do("DELETE FROM revzones WHERE rdns_id=?", undef, ($zoneid));
1956 }
[3]1957
[285]1958 $msg = "Deleted ".($revrec eq 'n' ? 'domain' : 'reverse zone')." $zone";
1959 $loghash{entry} = $msg;
[487]1960 $self->_log(%loghash);
[285]1961
[3]1962 # once we get here, we should have suceeded.
[23]1963 $dbh->commit;
[3]1964 }; # end eval
1965
1966 if ($@) {
[285]1967 $msg = $@;
[3]1968 eval { $dbh->rollback; };
[295]1969 $loghash{entry} = "Error deleting $zone: $msg ($failmsg)";
[517]1970 if ($self->{log_failures}) {
[487]1971 $self->_log(%loghash);
[295]1972 $dbh->commit; # since we enabled transactions earlier
1973 }
1974 return ('FAIL', $loghash{entry});
[3]1975 } else {
[295]1976 return ('OK', $msg);
[3]1977 }
1978
[274]1979} # end delZone()
[3]1980
1981
[2]1982## DNSDB::domainName()
1983# Return the domain name based on a domain ID
1984# Takes a database handle and the domain ID
1985# Returns the domain name or undef on failure
1986sub domainName {
1987 $errstr = '';
[473]1988 my $self = shift;
1989 my $dbh = $self->{dbh};
[2]1990 my $domid = shift;
[91]1991 my ($domname) = $dbh->selectrow_array("SELECT domain FROM domains WHERE domain_id=?", undef, ($domid) );
[2]1992 $errstr = $DBI::errstr if !$domname;
1993 return $domname if $domname;
[91]1994} # end domainName()
[2]1995
1996
[224]1997## DNSDB::revName()
1998# Return the reverse zone name based on an rDNS ID
1999# Takes a database handle and the rDNS ID
2000# Returns the reverse zone name or undef on failure
2001sub revName {
2002 $errstr = '';
[473]2003 my $self = shift;
2004 my $dbh = $self->{dbh};
[224]2005 my $revid = shift;
2006 my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) );
2007 $errstr = $DBI::errstr if !$revname;
2008 return $revname if $revname;
2009} # end revName()
2010
2011
[91]2012## DNSDB::domainID()
2013# Takes a database handle and domain name
2014# Returns the domain ID number
2015sub domainID {
2016 $errstr = '';
[473]2017 my $self = shift;
2018 my $dbh = $self->{dbh};
[91]2019 my $domain = shift;
[349]2020 my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)",
2021 undef, ($domain) );
[501]2022 if (!$domid) {
2023 if ($dbh->err) {
2024 $errstr = $DBI::errstr;
2025 } else {
2026 $errstr = "Domain $domain not present";
2027 }
2028 }
[91]2029 return $domid if $domid;
2030} # end domainID()
2031
2032
[276]2033## DNSDB::revID()
2034# Takes a database handle and reverse zone name
2035# Returns the rDNS ID number
2036sub revID {
2037 $errstr = '';
[473]2038 my $self = shift;
2039 my $dbh = $self->{dbh};
[276]2040 my $revzone = shift;
2041 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ($revzone) );
[501]2042 if (!$revid) {
2043 if ($dbh->err) {
2044 $errstr = $DBI::errstr;
2045 } else {
2046 $errstr = "Reverse zone $revzone not present";
2047 }
2048 }
[276]2049 return $revid if $revid;
2050} # end revID()
2051
2052
[260]2053## DNSDB::addRDNS
2054# Adds a reverse DNS zone
[286]2055# Takes a database handle, CIDR block, reverse DNS pattern, numeric group,
2056# and boolean(ish) state (active/inactive)
[260]2057# Returns a status code and message
2058sub addRDNS {
[477]2059 my $self = shift;
2060 my $dbh = $self->{dbh};
[260]2061 my $zone = NetAddr::IP->new(shift);
[446]2062
[260]2063 return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/);
[270]2064 my $revpatt = shift; # construct a custom (A/AAAA+)? PTR template record
[260]2065 my $group = shift;
2066 my $state = shift;
[446]2067 my $defloc = shift || '';
[260]2068
2069 $state = 1 if $state =~ /^active$/;
2070 $state = 1 if $state =~ /^on$/;
2071 $state = 0 if $state =~ /^inactive$/;
2072 $state = 0 if $state =~ /^off$/;
2073
2074 return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/;
2075
2076# quick check to start to see if we've already got one
[299]2077 my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ("$zone"));
[260]2078
2079 return ('FAIL', "Zone already exists") if $rdns_id;
2080
2081 # Allow transactions, and raise an exception on errors so we can catch it later.
2082 # Use local to make sure these get "reset" properly on exiting this block
2083 local $dbh->{AutoCommit} = 0;
2084 local $dbh->{RaiseError} = 1;
2085
[264]2086 my $warnstr = '';
[270]2087 my $defttl = 3600; # 1 hour should be reasonable. And unless things have gone horribly
2088 # wrong, we should have a value to override this anyway.
[264]2089
[260]2090 # Wrap all the SQL in a transaction
2091 eval {
[446]2092 # insert the zone...
2093 $dbh->do("INSERT INTO revzones (revnet,group_id,status,default_location) VALUES (?,?,?,?)", undef,
2094 ($zone, $group, $state, $defloc) );
[260]2095
2096 # get the ID...
2097 ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
2098
[487]2099 $self->_log(rdns_id => $rdns_id, group_id => $group,
2100 entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone");
[260]2101
2102 # ... and now we construct the standard records from the default set. NB: group should be variable.
2103 my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?");
[446]2104 my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,domain_id,host,type,val,ttl,location)".
2105 " VALUES ($rdns_id,?,?,?,?,?,?)");
[260]2106 $sth->execute($group);
2107 while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) {
[264]2108 # Silently skip v4/v6 mismatches. This is not an error, this is expected.
2109 if ($zone->{isv6}) {
2110 next if ($type == 65280 || $type == 65283);
2111 } else {
2112 next if ($type == 65281 || $type == 65284);
2113 }
[269]2114
[517]2115 $host =~ s/ADMINDOMAIN/$self->{domain}/g;
[264]2116
[265]2117 # Check to make sure the IP stubs will fit in the zone. Under most usage failures here should be rare.
2118 # On failure, tack a note on to a warning string and continue without adding this record.
2119 # While we're at it, we substitute $zone for ZONE in the value.
2120 if ($val eq 'ZONE') {
[444]2121 # If we've got a pattern, we skip the default record version on (A+)PTR-template types
2122 next if $revpatt && ($type == 65282 || $type == 65283);
[269]2123##fixme? do we care if we have multiple whole-zone templates?
[265]2124 $val = $zone->network;
2125 } elsif ($val =~ /ZONE/) {
2126 my $tmpval = $val;
2127 $tmpval =~ s/ZONE//;
[269]2128 # Bend the rules and allow single-trailing-number PTR or PTR template records to be inserted
2129 # as either v4 or v6. May make this an off-by-default config flag
2130 # Note that the origin records that may trigger this **SHOULD** already have ZONE,\d
2131 if ($type == 12 || $type == 65282) {
2132 $tmpval =~ s/[,.]/::/ if ($tmpval =~ /^[,.]\d+$/ && $zone->{isv6});
2133 $tmpval =~ s/[,:]+/./ if ($tmpval =~ /^(?:,|::)\d+$/ && !$zone->{isv6});
2134 }
[265]2135 my $addr;
[471]2136 if ($self->_ipparent('n', 'y', \$tmpval, $rdns_id, \$addr)) {
[265]2137 $val = $addr->addr;
2138 } else {
[269]2139 $warnstr .= "\nDefault record '$val $typemap{$type} $host' doesn't fit in $zone, skipping";
[264]2140 next;
2141 }
2142 }
2143
[328]2144 # Substitute $zone for ZONE in the hostname, but only for non-NS records.
2145 # NS records get this substitution on the value instead.
2146 $host = _ZONE($zone, $host) if $type != 2;
[265]2147
[269]2148 # Fill in the forward domain ID if we can find it, otherwise:
2149 # Coerce type down to PTR or PTR template if we can't
2150 my $domid = 0;
2151 if ($type >= 65280) {
[471]2152 if (!($domid = $self->_hostparent($host))) {
[269]2153 $warnstr .= "\nRecord added as PTR instead of $typemap{$type}; domain not found for $host";
2154 $type = $reverse_typemap{PTR};
2155 $domid = 0; # just to be explicit.
2156 }
2157 }
2158
[446]2159 $sth_in->execute($domid,$host,$type,$val,$ttl,$defloc);
[269]2160
[260]2161 if ($typemap{$type} eq 'SOA') {
2162 my @tmp1 = split /:/, $host;
2163 my @tmp2 = split /:/, $val;
[487]2164 $self->_log(rdns_id => $rdns_id, group_id => $group,
[286]2165 entry => "[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
[487]2166 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl");
[270]2167 $defttl = $tmp2[3];
[260]2168 } else {
[446]2169 my $logentry = "[new $zone] Added record '$host $typemap{$type} $val', TTL $ttl";
[480]2170 $logentry .= ", default location ".$self->getLoc($defloc)->{description} if $defloc;
[487]2171 $self->_log(rdns_id => $rdns_id, domain_id => $domid, group_id => $group, entry => $logentry);
[260]2172 }
2173 }
2174
[270]2175 # Generate record based on provided pattern.
2176 if ($revpatt) {
2177 my $host;
2178 my $type = ($zone->{isv6} ? 65284 : 65283);
2179 my $val = $zone->network;
[269]2180
[270]2181 # Substitute $zone for ZONE in the hostname.
2182 $host = _ZONE($zone, $revpatt);
2183
2184 my $domid = 0;
[471]2185 if (!($domid = $self->_hostparent($host))) {
[270]2186 $warnstr .= "\nDefault pattern added as PTR template instead of $typemap{$type}; domain not found for $host";
2187 $type = 65282;
2188 $domid = 0; # just to be explicit.
2189 }
2190
[446]2191 $sth_in->execute($domid,$host,$type,$val,$defttl,$defloc);
[286]2192 my $logentry = "[new $zone] Added record '$host $typemap{$type}";
[487]2193 $self->_log(rdns_id => $rdns_id, domain_id => $domid, group_id => $group,
2194 entry => $logentry." $val', TTL $defttl from pattern");
[270]2195 }
2196
[264]2197 # If there are warnings (presumably about default records skipped for cause) log them
[487]2198 $self->_log(rdns_id => $rdns_id, group_id => $group, entry => "Warning(s) adding $zone:$warnstr")
[264]2199 if $warnstr;
[269]2200
[260]2201 # once we get here, we should have suceeded.
2202 $dbh->commit;
2203 }; # end eval
2204
2205 if ($@) {
2206 my $msg = $@;
2207 eval { $dbh->rollback; };
[487]2208 $self->_log(group_id => $group, entry => "Failed adding reverse zone $zone ($msg)")
[517]2209 if $self->{log_failures};
[286]2210 $dbh->commit; # since we enabled transactions earlier
[260]2211 return ('FAIL',$msg);
2212 } else {
[286]2213 my $retcode = 'OK';
2214 if ($warnstr) {
2215 $resultstr = $warnstr;
2216 $retcode = 'WARN';
2217 }
2218 return ($retcode, $rdns_id);
[260]2219 }
2220
2221} # end addRDNS()
2222
2223
[237]2224## DNSDB::getZoneCount
2225# Get count of zones in group or groups
2226# Takes a database handle and hash containing:
2227# - the "current" group
2228# - an array of "acceptable" groups
2229# - a flag for forward/reverse zones
2230# - Optionally accept a "starts with" and/or "contains" filter argument
2231# Returns an integer count of the resulting zone list.
2232sub getZoneCount {
[477]2233 my $self = shift;
2234 my $dbh = $self->{dbh};
[237]2235
2236 my %args = @_;
2237
[509]2238 # Fail on bad curgroup argument. There's no sane fallback on this one.
2239 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
2240 $errstr = "Bad or missing curgroup argument";
2241 return;
2242 }
2243 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
2244 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
2245 $errstr = "Bad childlist argument";
2246 return;
2247 }
2248
[237]2249 my @filterargs;
[239]2250 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
2251 push @filterargs, "^$args{startwith}" if $args{startwith};
2252 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones
[237]2253 push @filterargs, $args{filter} if $args{filter};
2254
2255 my $sql;
2256 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read
2257 if ($args{revrec} eq 'n') {
2258 $sql = "SELECT count(*) FROM domains".
2259 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2260 ($args{startwith} ? " AND domain ~* ?" : '').
2261 ($args{filter} ? " AND domain ~* ?" : '');
2262 } else {
2263 $sql = "SELECT count(*) FROM revzones".
2264 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2265 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '').
2266 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '');
2267 }
2268 my ($count) = $dbh->selectrow_array($sql, undef, @filterargs);
2269 return $count;
2270} # end getZoneCount()
2271
2272
2273## DNSDB::getZoneList()
2274# Get a list of zones in the specified group(s)
2275# Takes the same arguments as getZoneCount() above
2276# Returns a reference to an array of hashrefs suitable for feeding to HTML::Template
2277sub getZoneList {
[477]2278 my $self = shift;
2279 my $dbh = $self->{dbh};
[237]2280
2281 my %args = @_;
2282
2283 my @zonelist;
2284
[475]2285 $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
[239]2286 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
[237]2287
[509]2288 # Fail on bad curgroup argument. There's no sane fallback on this one.
2289 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
2290 $errstr = "Bad or missing curgroup argument";
2291 return;
2292 }
2293 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
2294 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
2295 $errstr = "Bad childlist argument";
2296 return;
2297 }
2298
[237]2299 my @filterargs;
[239]2300 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
2301 push @filterargs, "^$args{startwith}" if $args{startwith};
2302 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones
[237]2303 push @filterargs, $args{filter} if $args{filter};
2304
2305 my $sql;
2306 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read
2307 if ($args{revrec} eq 'n') {
[475]2308 $args{sortby} = 'domain' if !$args{sortby} || !grep /^$args{sortby}$/, ('domain','group','status');
[237]2309 $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains".
2310 " INNER JOIN groups ON domains.group_id=groups.group_id".
2311 " WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2312 ($args{startwith} ? " AND domain ~* ?" : '').
2313 ($args{filter} ? " AND domain ~* ?" : '');
2314 } else {
[239]2315##fixme: arguably startwith here is irrelevant. depends on the UI though.
[475]2316 $args{sortby} = 'revnet' if !$args{sortby} || !grep /^$args{sortby}$/, ('revnet','group','status');
[237]2317 $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones".
2318 " INNER JOIN groups ON revzones.group_id=groups.group_id".
2319 " WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2320 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '').
2321 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : '');
2322 }
2323 # A common tail.
[239]2324 $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ".
[517]2325 ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage}".
2326 " OFFSET ".$args{offset}*$self->{perpage});
[402]2327 my $sth = $dbh->prepare($sql);
2328 $sth->execute(@filterargs);
2329 my $rownum = 0;
[237]2330
[402]2331 while (my @data = $sth->fetchrow_array) {
2332 my %row;
2333 $row{domain_id} = $data[0];
2334 $row{domain} = $data[1];
2335 $row{status} = $data[2];
2336 $row{group} = $data[3];
2337 push @zonelist, \%row;
2338 }
2339
2340 return \@zonelist;
[237]2341} # end getZoneList()
2342
2343
[383]2344## DNSDB::getZoneLocation()
[387]2345# Retrieve the default location for a zone.
[383]2346# Takes a database handle, forward/reverse flag, and zone ID
2347sub getZoneLocation {
[477]2348 my $self = shift;
2349 my $dbh = $self->{dbh};
[383]2350 my $revrec = shift;
2351 my $zoneid = shift;
2352
2353 my ($loc) = $dbh->selectrow_array("SELECT default_location FROM ".
2354 ($revrec eq 'n' ? 'domains WHERE domain_id = ?' : 'revzones WHERE rdns_id = ?'),
2355 undef, ($zoneid));
2356 return $loc;
2357} # end getZoneLocation()
2358
2359
[18]2360## DNSDB::addGroup()
2361# Add a group
[66]2362# Takes a database handle, group name, parent group, hashref for permissions,
[292]2363# and optional template-vs-cloneme flag for the default records
[18]2364# Returns a status code and message
2365sub addGroup {
2366 $errstr = '';
[476]2367 my $self = shift;
2368 my $dbh = $self->{dbh};
[20]2369 my $groupname = shift;
2370 my $pargroup = shift;
[66]2371 my $permissions = shift;
[18]2372
[66]2373 # 0 indicates "custom", hardcoded.
[18]2374 # Any other value clones that group's default records, if it exists.
[66]2375 my $inherit = shift || 0;
2376##fixme: need a flag to indicate clone records or <?> ?
[18]2377
2378 # Allow transactions, and raise an exception on errors so we can catch it later.
2379 # Use local to make sure these get "reset" properly on exiting this block
2380 local $dbh->{AutoCommit} = 0;
2381 local $dbh->{RaiseError} = 1;
2382
[292]2383 my ($group_id) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname));
[38]2384
2385 return ('FAIL', "Group already exists") if $group_id;
2386
[18]2387 # Wrap all the SQL in a transaction
2388 eval {
[292]2389 $dbh->do("INSERT INTO groups (parent_group_id,group_name) VALUES (?,?)", undef, ($pargroup, $groupname) );
[18]2390
[292]2391 my ($groupid) = $dbh->selectrow_array("SELECT currval('groups_group_id_seq')");
[18]2392
[292]2393 # We work through the whole set of permissions instead of specifying them so
2394 # that when we add a new permission, we don't have to change the code anywhere
2395 # that doesn't explicitly deal with that specific permission.
2396 my @permvals;
2397 foreach (@permtypes) {
2398 if (!defined ($permissions->{$_})) {
2399 push @permvals, 0;
2400 } else {
2401 push @permvals, $permissions->{$_};
[66]2402 }
[292]2403 }
2404 $dbh->do("INSERT INTO permissions (group_id,$permlist) values (?".',?'x($#permtypes+1).")",
2405 undef, ($groupid, @permvals) );
2406 my ($permid) = $dbh->selectrow_array("SELECT currval('permissions_permission_id_seq')");
2407 $dbh->do("UPDATE groups SET permission_id=$permid WHERE group_id=$groupid");
[66]2408
[292]2409 # Default records
[255]2410 my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
[20]2411 "VALUES ($groupid,?,?,?,?,?,?,?)");
[255]2412 my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ".
2413 "VALUES ($groupid,?,?,?,?)");
[66]2414 if ($inherit) {
[87]2415 # Duplicate records from parent. Actually relying on inherited records feels
2416 # very fragile, and it would be problematic to roll over at a later time.
[18]2417 my $sth2 = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?");
[87]2418 $sth2->execute($pargroup);
[18]2419 while (my @clonedata = $sth2->fetchrow_array) {
[255]2420 $sthf->execute(@clonedata);
[18]2421 }
[255]2422 # And now the reverse records
[292]2423 $sth2 = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?");
[255]2424 $sth2->execute($pargroup);
2425 while (my @clonedata = $sth2->fetchrow_array) {
2426 $sthr->execute(@clonedata);
2427 }
[18]2428 } else {
[66]2429##fixme: Hardcoding is Bad, mmmmkaaaay?
[18]2430 # reasonable basic defaults for SOA, MX, NS, and minimal hosting
2431 # could load from a config file, but somewhere along the line we need hardcoded bits.
[255]2432 $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
2433 $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
2434 $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
2435 $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
2436 $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
2437 $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
2438 # reasonable basic defaults for generic reverse zone. Same as initial SQL tabledef.
2439 $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400);
2440 $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600);
[18]2441 }
2442
[487]2443 $self->_log(group_id => $pargroup, entry => "Added group $groupname");
[292]2444
[18]2445 # once we get here, we should have suceeded.
2446 $dbh->commit;
2447 }; # end eval
2448
2449 if ($@) {
2450 my $msg = $@;
2451 eval { $dbh->rollback; };
[517]2452 if ($self->{log_failures}) {
[487]2453 $self->_log(group_id => $pargroup, entry => "Failed to add group $groupname: $msg");
[292]2454 $dbh->commit;
2455 }
[18]2456 return ('FAIL',$msg);
2457 }
2458
[292]2459 return ('OK','OK');
[18]2460} # end addGroup()
2461
2462
[22]2463## DNSDB::delGroup()
2464# Delete a group.
2465# Takes a group ID
2466# Returns a status code and message
2467sub delGroup {
[476]2468 my $self = shift;
2469 my $dbh = $self->{dbh};
[22]2470 my $groupid = shift;
2471
2472 # Allow transactions, and raise an exception on errors so we can catch it later.
2473 # Use local to make sure these get "reset" properly on exiting this block
2474 local $dbh->{AutoCommit} = 0;
2475 local $dbh->{RaiseError} = 1;
2476
2477##fixme: locate "knowable" error conditions and deal with them before the eval
[23]2478# ... or inside, whatever.
[22]2479# -> domains still exist in group
2480# -> ...
[23]2481 my $failmsg = '';
[293]2482 my $resultmsg = '';
[22]2483
[293]2484 # collect some pieces for logging and error messages
[473]2485 my $groupname = $self->groupName($groupid);
2486 my $parid = $self->parentID(id => $groupid, type => 'group');
[293]2487
[22]2488 # Wrap all the SQL in a transaction
2489 eval {
[293]2490 # Check for Things in the group
2491 $failmsg = "Can't remove group $groupname";
2492 my ($grpcnt) = $dbh->selectrow_array("SELECT count(*) FROM groups WHERE parent_group_id=?", undef, ($groupid));
2493 die "$grpcnt groups still in group\n" if $grpcnt;
2494 my ($domcnt) = $dbh->selectrow_array("SELECT count(*) FROM domains WHERE group_id=?", undef, ($groupid));
[23]2495 die "$domcnt domains still in group\n" if $domcnt;
[512]2496 my ($revcnt) = $dbh->selectrow_array("SELECT count(*) FROM revzones WHERE group_id=?", undef, ($groupid));
2497 die "$revcnt reverse zones still in group\n" if $revcnt;
[293]2498 my ($usercnt) = $dbh->selectrow_array("SELECT count(*) FROM users WHERE group_id=?", undef, ($groupid));
2499 die "$usercnt users still in group\n" if $usercnt;
[23]2500
[293]2501 $failmsg = "Failed to delete default records for $groupname";
2502 $dbh->do("DELETE from default_records WHERE group_id=?", undef, ($groupid));
2503 $failmsg = "Failed to delete default reverse records for $groupname";
2504 $dbh->do("DELETE from default_rev_records WHERE group_id=?", undef, ($groupid));
2505 $failmsg = "Failed to remove group $groupname";
2506 $dbh->do("DELETE from groups WHERE group_id=?", undef, ($groupid));
[22]2507
[487]2508 $self->_log(group_id => $parid, entry => "Deleted group $groupname");
[293]2509 $resultmsg = "Deleted group $groupname";
2510
[22]2511 # once we get here, we should have suceeded.
2512 $dbh->commit;
2513 }; # end eval
2514
2515 if ($@) {
2516 my $msg = $@;
2517 eval { $dbh->rollback; };
[517]2518 if ($self->{log_failures}) {
[487]2519 $self->_log(group_id => $parid, entry => "$failmsg: $msg");
[293]2520 $dbh->commit; # since we enabled transactions earlier
2521 }
[23]2522 return ('FAIL',"$failmsg: $msg");
[22]2523 }
[293]2524
2525 return ('OK',$resultmsg);
[22]2526} # end delGroup()
2527
2528
[19]2529## DNSDB::getChildren()
2530# Get a list of all groups whose parent^n is group <n>
[24]2531# Takes a database handle, group ID, reference to an array to put the group IDs in,
2532# and an optional flag to return only immediate children or all children-of-children
2533# default to returning all children
[19]2534# Calls itself
2535sub getChildren {
2536 $errstr = '';
[476]2537 my $self = shift;
2538 my $dbh = $self->{dbh};
[20]2539 my $rootgroup = shift;
2540 my $groupdest = shift;
[24]2541 my $immed = shift || 'all';
[19]2542
2543 # special break for default group; otherwise we get stuck.
[20]2544 if ($rootgroup == 1) {
[19]2545 # by definition, group 1 is the Root Of All Groups
[24]2546 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE NOT (group_id=1)".
[331]2547 ($immed ne 'all' ? " AND parent_group_id=1" : '')." ORDER BY group_name");
[19]2548 $sth->execute;
2549 while (my @this = $sth->fetchrow_array) {
[20]2550 push @$groupdest, @this;
[19]2551 }
2552 } else {
[331]2553 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE parent_group_id=? ORDER BY group_name");
[20]2554 $sth->execute($rootgroup);
[19]2555 return if $sth->rows == 0;
[20]2556 my @grouplist;
2557 while (my ($group) = $sth->fetchrow_array) {
2558 push @$groupdest, $group;
[476]2559 $self->getChildren($group, $groupdest) if $immed eq 'all';
[19]2560 }
2561 }
2562} # end getChildren()
2563
2564
[20]2565## DNSDB::groupName()
[17]2566# Return the group name based on a group ID
2567# Takes a database handle and the group ID
2568# Returns the group name or undef on failure
[20]2569sub groupName {
[13]2570 $errstr = '';
[473]2571 my $self = shift;
2572 my $dbh = $self->{dbh};
[20]2573 my $groupid = shift;
2574 my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?");
2575 $sth->execute($groupid);
2576 my ($groupname) = $sth->fetchrow_array();
2577 $errstr = $DBI::errstr if !$groupname;
2578 return $groupname if $groupname;
2579} # end groupName
[13]2580
2581
[314]2582## DNSDB::getGroupCount()
2583# Get count of subgroups in group or groups
2584# Takes a database handle and hash containing:
2585# - the "current" group
2586# - an array of "acceptable" groups
2587# - Optionally accept a "starts with" and/or "contains" filter argument
2588# Returns an integer count of the resulting group list.
2589sub getGroupCount {
[476]2590 my $self = shift;
2591 my $dbh = $self->{dbh};
[314]2592
2593 my %args = @_;
2594
[509]2595 # Fail on bad curgroup argument. There's no sane fallback on this one.
2596 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
2597 $errstr = "Bad or missing curgroup argument";
2598 return;
2599 }
2600 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
2601 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
2602 $errstr = "Bad childlist argument";
2603 return;
2604 }
2605
[314]2606 my @filterargs;
2607 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
2608 push @filterargs, "^$args{startwith}" if $args{startwith};
2609 push @filterargs, $args{filter} if $args{filter};
2610
2611 my $sql = "SELECT count(*) FROM groups ".
2612 "WHERE parent_group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2613 ($args{startwith} ? " AND group_name ~* ?" : '').
2614 ($args{filter} ? " AND group_name ~* ?" : '');
2615 my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) );
2616 $errstr = $dbh->errstr if !$count;
2617 return $count;
2618} # end getGroupCount
2619
2620
2621## DNSDB::getGroupList()
2622# Get a list of sub^n-groups in the specified group(s)
2623# Takes the same arguments as getGroupCount() above
2624# Returns an arrayref containing hashrefs suitable for feeding straight to HTML::Template
2625sub getGroupList {
[476]2626 my $self = shift;
2627 my $dbh = $self->{dbh};
[314]2628
2629 my %args = @_;
2630
[509]2631 # Fail on bad curgroup argument. There's no sane fallback on this one.
2632 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
2633 $errstr = "Bad or missing curgroup argument";
2634 return;
2635 }
2636 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
2637 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
2638 $errstr = "Bad childlist argument";
2639 return;
2640 }
2641
[314]2642 my @filterargs;
2643 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
2644 push @filterargs, "^$args{startwith}" if $args{startwith};
2645 push @filterargs, $args{filter} if $args{filter};
2646
2647 # protection against bad or missing arguments
[508]2648 $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
2649 $args{sortby} = 'group' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
[397]2650 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
[314]2651
2652 # munge sortby for columns in database
2653 $args{sortby} = 'g.group_name' if $args{sortby} eq 'group';
2654 $args{sortby} = 'g2.group_name' if $args{sortby} eq 'parent';
2655
[385]2656 my $sql = q(SELECT g.group_id AS groupid, g.group_name AS groupname, g2.group_name AS pgroup
[314]2657 FROM groups g
2658 INNER JOIN groups g2 ON g2.group_id=g.parent_group_id
2659 ).
[385]2660 " WHERE g.parent_group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
[314]2661 ($args{startwith} ? " AND g.group_name ~* ?" : '').
2662 ($args{filter} ? " AND g.group_name ~* ?" : '').
2663 " GROUP BY g.group_id, g.group_name, g2.group_name ".
2664 " ORDER BY $args{sortby} $args{sortorder} ".
[517]2665 ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
[314]2666 my $glist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) );
2667 $errstr = $dbh->errstr if !$glist;
[385]2668
2669 # LEFT JOINs make the result set balloon beyond sanity just to include counts;
2670 # this means there's lots of crunching needed to trim the result set back down.
2671 # So instead we track the order of the groups, and push the counts into the
2672 # arrayref result separately.
2673##fixme: put this whole sub in a transaction? might be
2674# needed for accurate results on very busy systems.
[386]2675##fixme: large group lists need prepared statements?
2676#my $ucsth = $dbh->prepare("SELECT count(*) FROM users WHERE group_id=?");
2677#my $dcsth = $dbh->prepare("SELECT count(*) FROM domains WHERE group_id=?");
2678#my $rcsth = $dbh->prepare("SELECT count(*) FROM revzones WHERE group_id=?");
[385]2679 foreach (@{$glist}) {
[386]2680 my ($ucnt) = $dbh->selectrow_array("SELECT count(*) FROM users WHERE group_id=?", undef, ($$_{groupid}));
2681 $$_{nusers} = $ucnt;
2682 my ($dcnt) = $dbh->selectrow_array("SELECT count(*) FROM domains WHERE group_id=?", undef, ($$_{groupid}));
2683 $$_{ndomains} = $dcnt;
2684 my ($rcnt) = $dbh->selectrow_array("SELECT count(*) FROM revzones WHERE group_id=?", undef, ($$_{groupid}));
2685 $$_{nrevzones} = $rcnt;
[385]2686 }
2687
[314]2688 return $glist;
2689} # end getGroupList
2690
2691
[118]2692## DNSDB::groupID()
2693# Return the group ID based on the group name
2694# Takes a database handle and the group name
2695# Returns the group ID or undef on failure
2696sub groupID {
2697 $errstr = '';
[473]2698 my $self = shift;
2699 my $dbh = $self->{dbh};
[118]2700 my $group = shift;
[406]2701 my ($grpid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($group) );
[118]2702 $errstr = $DBI::errstr if !$grpid;
2703 return $grpid if $grpid;
2704} # end groupID()
2705
2706
[24]2707## DNSDB::addUser()
[87]2708# Add a user.
2709# Takes a DB handle, username, group ID, password, state (active/inactive).
2710# Optionally accepts:
2711# user type (user/admin) - defaults to user
2712# permissions string - defaults to inherit from group
2713# three valid forms:
2714# i - Inherit permissions
2715# c:<user_id> - Clone permissions from <user_id>
2716# C:<permission list> - Set these specific permissions
2717# first name - defaults to username
2718# last name - defaults to blank
2719# phone - defaults to blank (could put other data within column def)
[90]2720# Returns (OK,<uid>) on success, (FAIL,<message>) on failure
[24]2721sub addUser {
2722 $errstr = '';
[479]2723 my $self = shift;
2724 my $dbh = $self->{dbh};
[24]2725 my $username = shift;
2726 my $group = shift;
2727 my $pass = shift;
2728 my $state = shift;
[25]2729
[90]2730 return ('FAIL', "Missing one or more required entries") if !defined($state);
2731 return ('FAIL', "Username must not be blank") if !$username;
[87]2732
[408]2733 # Munge in some alternate state values
2734 $state = 1 if $state =~ /^active$/;
2735 $state = 1 if $state =~ /^on$/;
2736 $state = 0 if $state =~ /^inactive$/;
2737 $state = 0 if $state =~ /^off$/;
2738
[25]2739 my $type = shift || 'u'; # create limited users by default - fwiw, not sure yet how this will interact with ACLs
2740
[67]2741 my $permstring = shift || 'i'; # default is to inhert permissions from group
2742
[25]2743 my $fname = shift || $username;
[24]2744 my $lname = shift || '';
[25]2745 my $phone = shift || ''; # not going format-check
[24]2746
[38]2747 my $sth = $dbh->prepare("SELECT user_id FROM users WHERE username=?");
[24]2748 my $user_id;
2749
[38]2750# quick check to start to see if we've already got one
2751 $sth->execute($username);
2752 ($user_id) = $sth->fetchrow_array;
2753
2754 return ('FAIL', "User already exists") if $user_id;
2755
[24]2756 # Allow transactions, and raise an exception on errors so we can catch it later.
2757 # Use local to make sure these get "reset" properly on exiting this block
2758 local $dbh->{AutoCommit} = 0;
2759 local $dbh->{RaiseError} = 1;
2760
2761 # Wrap all the SQL in a transaction
2762 eval {
[87]2763 # insert the user... note we set inherited perms by default since
2764 # it's simple and cleans up some other bits of state
2765 my $sth = $dbh->prepare("INSERT INTO users ".
2766 "(group_id,username,password,firstname,lastname,phone,type,status,permission_id,inherit_perm) ".
2767 "VALUES (?,?,?,?,?,?,?,?,(SELECT permission_id FROM permissions WHERE group_id=?),'t')");
2768 $sth->execute($group,$username,unix_md5_crypt($pass),$fname,$lname,$phone,$type,$state,$group);
[24]2769
2770 # get the ID...
[94]2771 ($user_id) = $dbh->selectrow_array("SELECT currval('users_user_id_seq')");
[24]2772
[87]2773# Permissions! Gotta set'em all!
[401]2774 die "Invalid permission string $permstring\n"
[87]2775 if $permstring !~ /^(?:
2776 i # inherit
2777 |c:\d+ # clone
2778 # custom. no, the leading , is not a typo
[391]2779 |C:(?:,(?:group|user|domain|record|location|self)_(?:edit|create|delete|locchg|view))*
[87]2780 )$/x;
2781# bleh. I'd call another function to do my dirty work, but we're in the middle of a transaction already.
2782 if ($permstring ne 'i') {
2783 # for cloned or custom permissions, we have to create a new permissions entry.
2784 my $clonesrc = $group;
2785 if ($permstring =~ /^c:(\d+)/) { $clonesrc = $1; }
2786 $dbh->do("INSERT INTO permissions ($permlist,user_id) ".
2787 "SELECT $permlist,? FROM permissions WHERE permission_id=".
2788 "(SELECT permission_id FROM permissions WHERE ".($permstring =~ /^c:/ ? 'user' : 'group')."_id=?)",
2789 undef, ($user_id,$clonesrc) );
2790 $dbh->do("UPDATE users SET permission_id=".
2791 "(SELECT permission_id FROM permissions WHERE user_id=?) ".
2792 "WHERE user_id=?", undef, ($user_id, $user_id) );
2793 }
2794 if ($permstring =~ /^C:/) {
2795 # finally for custom permissions, we set the passed-in permissions (and unset
2796 # any that might have been brought in by the clone operation above)
2797 my ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions WHERE user_id=?",
2798 undef, ($user_id) );
2799 foreach (@permtypes) {
2800 if ($permstring =~ /,$_/) {
2801 $dbh->do("UPDATE permissions SET $_='t' WHERE permission_id=?", undef, ($permid) );
2802 } else {
2803 $dbh->do("UPDATE permissions SET $_='f' WHERE permission_id=?", undef, ($permid) );
2804 }
2805 }
2806 }
2807
2808 $dbh->do("UPDATE users SET inherit_perm='n' WHERE user_id=?", undef, ($user_id) );
2809
[25]2810##fixme: add another table to hold name/email for log table?
2811
[487]2812 $self->_log(group_id => $group, entry => "Added user $username ($fname $lname)");
[24]2813 # once we get here, we should have suceeded.
2814 $dbh->commit;
2815 }; # end eval
2816
2817 if ($@) {
2818 my $msg = $@;
2819 eval { $dbh->rollback; };
[517]2820 if ($self->{log_failures}) {
[487]2821 $self->_log(group_id => $group, entry => "Error adding user $username: $msg");
[294]2822 $dbh->commit; # since we enabled transactions earlier
2823 }
2824 return ('FAIL',"Error adding user $username: $msg");
[24]2825 }
[294]2826
2827 return ('OK',"User $username ($fname $lname) added");
[24]2828} # end addUser
2829
2830
[325]2831## DNSDB::getUserCount()
2832# Get count of users in group
2833# Takes a database handle and hash containing at least the current group, and optionally:
2834# - a reference list of secondary groups
2835# - a filter string
2836# - a "Starts with" string
2837sub getUserCount {
[479]2838 my $self = shift;
2839 my $dbh = $self->{dbh};
[325]2840
2841 my %args = @_;
2842
[509]2843 # Fail on bad curgroup argument. There's no sane fallback on this one.
2844 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
2845 $errstr = "Bad or missing curgroup argument";
2846 return;
2847 }
2848 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
2849 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
2850 $errstr = "Bad childlist argument";
2851 return;
2852 }
2853
[325]2854 my @filterargs;
2855 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
2856 push @filterargs, "^$args{startwith}" if $args{startwith};
2857 push @filterargs, $args{filter} if $args{filter};
2858
2859 my $sql = "SELECT count(*) FROM users ".
2860 "WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2861 ($args{startwith} ? " AND username ~* ?" : '').
2862 ($args{filter} ? " AND username ~* ?" : '');
2863 my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) );
2864 $errstr = $dbh->errstr if !$count;
2865 return $count;
2866} # end getUserCount()
2867
2868
2869## DNSDB::getUserList()
2870# Get list of users
2871# Takes the same arguments as getUserCount() above, plus optional:
2872# - sort field
2873# - sort order
2874# - offset/return-all-everything flag (defaults to $perpage records)
2875sub getUserList {
[479]2876 my $self = shift;
2877 my $dbh = $self->{dbh};
[325]2878
2879 my %args = @_;
2880
[509]2881 # Fail on bad curgroup argument. There's no sane fallback on this one.
2882 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
2883 $errstr = "Bad or missing curgroup argument";
2884 return;
2885 }
2886 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
2887 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
2888 $errstr = "Bad childlist argument";
2889 return;
2890 }
2891
[325]2892 my @filterargs;
2893 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
2894 push @filterargs, "^$args{startwith}" if $args{startwith};
2895 push @filterargs, $args{filter} if $args{filter};
2896
2897 # better to request sorts on "simple" names, but it means we need to map it to real columns
2898 my %sortmap = (user => 'u.username', type => 'u.type', group => 'g.group_name', status => 'u.status',
2899 fname => 'fname');
2900 $args{sortby} = $sortmap{$args{sortby}};
2901
2902 # protection against bad or missing arguments
[508]2903 $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
2904 $args{sortby} = 'u.username' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
[397]2905 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
[325]2906
2907 my $sql = "SELECT u.user_id, u.username, u.firstname || ' ' || u.lastname AS fname, u.type, g.group_name, u.status ".
2908 "FROM users u ".
2909 "INNER JOIN groups g ON u.group_id=g.group_id ".
2910 "WHERE u.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
2911 ($args{startwith} ? " AND u.username ~* ?" : '').
2912 ($args{filter} ? " AND u.username ~* ?" : '').
[401]2913 " AND NOT u.type = 'R' ".
[325]2914 " ORDER BY $args{sortby} $args{sortorder} ".
[517]2915 ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
[325]2916 my $ulist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) );
2917 $errstr = $dbh->errstr if !$ulist;
2918 return $ulist;
2919} # end getUserList()
2920
2921
[326]2922## DNSDB::getUserDropdown()
2923# Get a list of usernames for use in a dropdown menu.
2924# Takes a database handle, current group, and optional "tag this as selected" flag.
2925# Returns a reference to a list of hashrefs suitable to feeding to HTML::Template
2926sub getUserDropdown {
[479]2927 my $self = shift;
2928 my $dbh = $self->{dbh};
[326]2929 my $grp = shift;
2930 my $sel = shift || 0;
2931
2932 my $sth = $dbh->prepare("SELECT username,user_id FROM users WHERE group_id=?");
2933 $sth->execute($grp);
2934
2935 my @userlist;
2936 while (my ($username,$uid) = $sth->fetchrow_array) {
2937 my %row = (
2938 username => $username,
2939 uid => $uid,
2940 selected => ($sel == $uid ? 1 : 0)
2941 );
2942 push @userlist, \%row;
2943 }
2944 return \@userlist;
2945} # end getUserDropdown()
2946
2947
[83]2948## DNSDB:: updateUser()
[90]2949# Update general data about user
[83]2950sub updateUser {
[479]2951 my $self = shift;
2952 my $dbh = $self->{dbh};
[118]2953
2954##fixme: tweak calling convention so that we can update any given bit of data
[83]2955 my $uid = shift;
2956 my $username = shift;
2957 my $group = shift;
2958 my $pass = shift;
2959 my $state = shift;
[87]2960 my $type = shift || 'u';
[83]2961 my $fname = shift || $username;
2962 my $lname = shift || '';
2963 my $phone = shift || ''; # not going format-check
2964
[294]2965 my $resultmsg = '';
[83]2966
[411]2967 # Munge in some alternate state values
2968 $state = 1 if $state =~ /^active$/;
2969 $state = 1 if $state =~ /^on$/;
2970 $state = 0 if $state =~ /^inactive$/;
2971 $state = 0 if $state =~ /^off$/;
2972
[83]2973 # Allow transactions, and raise an exception on errors so we can catch it later.
2974 # Use local to make sure these get "reset" properly on exiting this block
2975 local $dbh->{AutoCommit} = 0;
2976 local $dbh->{RaiseError} = 1;
2977
2978 my $sth;
2979
2980 # Password can be left blank; if so we assume there's one on file.
2981 # Actual blank passwords are bad, mm'kay?
2982 if (!$pass) {
[294]2983 ($pass) = $dbh->selectrow_array("SELECT password FROM users WHERE user_id=?", undef, ($uid));
[83]2984 } else {
2985 $pass = unix_md5_crypt($pass);
2986 }
2987
2988 eval {
[294]2989 $dbh->do("UPDATE users SET username=?, password=?, firstname=?, lastname=?, phone=?, type=?, status=?".
2990 " WHERE user_id=?", undef, ($username, $pass, $fname, $lname, $phone, $type, $state, $uid));
2991 $resultmsg = "Updated user info for $username ($fname $lname)";
[487]2992 $self->_log(group_id => $group, entry => $resultmsg);
[83]2993 $dbh->commit;
2994 };
2995 if ($@) {
2996 my $msg = $@;
2997 eval { $dbh->rollback; };
[517]2998 if ($self->{log_failures}) {
[487]2999 $self->_log(group_id => $group, entry => "Error updating user $username: $msg");
[294]3000 $dbh->commit; # since we enabled transactions earlier
3001 }
3002 return ('FAIL',"Error updating user $username: $msg");
[83]3003 }
[294]3004
3005 return ('OK',$resultmsg);
[83]3006} # end updateUser()
3007
3008
[24]3009## DNSDB::delUser()
[297]3010# Delete a user.
3011# Takes a database handle and user ID
3012# Returns a success/failure code and matching message
[24]3013sub delUser {
[479]3014 my $self = shift;
3015 my $dbh = $self->{dbh};
[25]3016 my $userid = shift;
3017
[297]3018 return ('FAIL',"Bad userid") if !defined($userid);
[25]3019
[479]3020 my $userdata = $self->getUserData($userid);
[25]3021
[297]3022 # Allow transactions, and raise an exception on errors so we can catch it later.
3023 # Use local to make sure these get "reset" properly on exiting this block
3024 local $dbh->{AutoCommit} = 0;
3025 local $dbh->{RaiseError} = 1;
[25]3026
[297]3027 eval {
3028 $dbh->do("DELETE FROM users WHERE user_id=?", undef, ($userid));
[487]3029 $self->_log(group_id => $userdata->{group_id},
[297]3030 entry => "Deleted user ID $userid/".$userdata->{username}.
[487]3031 " (".$userdata->{firstname}." ".$userdata->{lastname}.")");
[297]3032 $dbh->commit;
3033 };
3034 if ($@) {
3035 my $msg = $@;
3036 eval { $dbh->rollback; };
[517]3037 if ($self->{log_failures}) {
[487]3038 $self->_log(group_id => $userdata->{group_id}, entry => "Error deleting user ID ".
3039 "$userid/".$userdata->{username}.": $msg");
[297]3040 $dbh->commit;
3041 }
3042 return ('FAIL',"Error deleting user $userid/".$userdata->{username}.": $msg");
3043 }
3044
3045 return ('OK',"Deleted user ".$userdata->{username}." (".$userdata->{firstname}." ".$userdata->{lastname}.")");
[24]3046} # end delUser
3047
3048
[25]3049## DNSDB::userFullName()
3050# Return a pretty string!
3051# Takes a user_id and optional printf-ish string to indicate which pieces where:
3052# %u for the username
3053# %f for the first name
3054# %l for the last name
3055# All other text in the passed string will be left as-is.
3056##fixme: need a "smart" option too, so that missing/null/blank first/last names don't give funky output
3057sub userFullName {
3058 $errstr = '';
[473]3059 my $self = shift;
3060 my $dbh = $self->{dbh};
[25]3061 my $userid = shift;
3062 my $fullformat = shift || '%f %l (%u)';
3063 my $sth = $dbh->prepare("select username,firstname,lastname from users where user_id=?");
3064 $sth->execute($userid);
3065 my ($uname,$fname,$lname) = $sth->fetchrow_array();
3066 $errstr = $DBI::errstr if !$uname;
3067
3068 $fullformat =~ s/\%u/$uname/g;
3069 $fullformat =~ s/\%f/$fname/g;
3070 $fullformat =~ s/\%l/$lname/g;
3071
3072 return $fullformat;
3073} # end userFullName
3074
3075
[51]3076## DNSDB::userStatus()
3077# Sets and/or returns a user's status
3078# Takes a database handle, user ID and optionally a status argument
3079# Returns undef on errors.
3080sub userStatus {
[479]3081 my $self = shift;
3082 my $dbh = $self->{dbh};
[51]3083 my $id = shift;
[296]3084 my $newstatus = shift || 'mu';
[51]3085
3086 return undef if $id !~ /^\d+$/;
3087
[479]3088 my $userdata = $self->getUserData($id);
[51]3089
[296]3090 # Allow transactions, and raise an exception on errors so we can catch it later.
3091 # Use local to make sure these get "reset" properly on exiting this block
3092 local $dbh->{AutoCommit} = 0;
3093 local $dbh->{RaiseError} = 1;
3094
3095 if ($newstatus ne 'mu') {
3096 # ooo, fun! let's see what we were passed for status
3097 eval {
3098 $newstatus = 0 if $newstatus eq 'useroff';
3099 $newstatus = 1 if $newstatus eq 'useron';
3100 $dbh->do("UPDATE users SET status=? WHERE user_id=?", undef, ($newstatus, $id));
3101
[297]3102 $resultstr = ($newstatus ? 'Enabled' : 'Disabled')." user ".$userdata->{username}.
3103 " (".$userdata->{firstname}." ".$userdata->{lastname}.")";
[296]3104
3105 my %loghash;
[473]3106 $loghash{group_id} = $self->parentID(id => $id, type => 'user');
[296]3107 $loghash{entry} = $resultstr;
[487]3108 $self->_log(%loghash);
[296]3109
3110 $dbh->commit;
3111 };
3112 if ($@) {
3113 my $msg = $@;
3114 eval { $dbh->rollback; };
3115 $resultstr = '';
3116 $errstr = $msg;
3117##fixme: failure logging?
3118 return;
[51]3119 }
3120 }
3121
[296]3122 my ($status) = $dbh->selectrow_array("SELECT status FROM users WHERE user_id=?", undef, ($id));
[51]3123 return $status;
3124} # end userStatus()
3125
3126
[83]3127## DNSDB::getUserData()
3128# Get misc user data for display
3129sub getUserData {
[479]3130 my $self = shift;
3131 my $dbh = $self->{dbh};
[83]3132 my $uid = shift;
3133
3134 my $sth = $dbh->prepare("SELECT group_id,username,firstname,lastname,phone,type,status,inherit_perm ".
3135 "FROM users WHERE user_id=?");
3136 $sth->execute($uid);
3137 return $sth->fetchrow_hashref();
3138} # end getUserData()
3139
3140
[370]3141## DNSDB::addLoc()
[375]3142# Add a new location.
3143# Takes a database handle, group ID, short and long description, and a comma-separated
3144# list of IP addresses.
3145# Returns ('OK',<location>) on success, ('FAIL',<failmsg>) on failure
3146sub addLoc {
[480]3147 my $self = shift;
3148 my $dbh = $self->{dbh};
[375]3149 my $grp = shift;
3150 my $shdesc = shift;
3151 my $comments = shift;
3152 my $iplist = shift;
[370]3153
[375]3154 # $shdesc gets set to the generated location ID if possible, but these can be de-undefined here.
3155 $comments = '' if !$comments;
3156 $iplist = '' if !$iplist;
3157
3158 my $loc;
3159
3160 # Generate a location ID. This is, by spec, a two-character widget. We'll use [a-z][a-z]
3161 # for now; 676 locations should satisfy all but the largest of the huge networks.
3162 # Not sure whether these are case-sensitive, or what other rules might apply - in any case
3163 # the absolute maximum is 16K (256*256) since it's parsed by tinydns as a two-character field.
3164
3165# add just after "my $origloc = $loc;":
3166# # These expand the possible space from 26^2 to 52^2 [* note in testing only 2052 were achieved],
3167# # and wrap it around.
3168# # Yes, they skip a couple of possibles. No, I don't care.
3169# $loc = 'aA' if $loc eq 'zz';
3170# $loc = 'Aa' if $loc eq 'zZ';
3171# $loc = 'ZA' if $loc eq 'Zz';
3172# $loc = 'aa' if $loc eq 'ZZ';
3173
3174 # Allow transactions, and raise an exception on errors so we can catch it later.
3175 # Use local to make sure these get "reset" properly on exiting this block
3176 local $dbh->{AutoCommit} = 0;
3177 local $dbh->{RaiseError} = 1;
3178
3179##fixme: There is probably a far better way to do this. Sequential increments
3180# are marginally less stupid that pure random generation though, and the existence
3181# check makes sure we don't stomp on an imported one.
3182
3183 eval {
3184 # Get the "last" location. Note this is the only use for loc_id, because selecting on location Does Funky Things
3185 ($loc) = $dbh->selectrow_array("SELECT location FROM locations ORDER BY loc_id DESC LIMIT 1");
[478]3186 ($loc) = ($loc =~ /^(..)/) if $loc;
[375]3187 my $origloc = $loc;
[446]3188 $loc = 'aa' if !$loc;
[375]3189 # Make a change...
3190 $loc++;
3191 # ... and keep changing if it exists
3192 while ($dbh->selectrow_array("SELECT count(*) FROM locations WHERE location LIKE ?", undef, ($loc.'%'))) {
3193 $loc++;
3194 ($loc) = ($loc =~ /^(..)/);
3195 die "too many locations in use, can't add another one\n" if $loc eq $origloc;
3196##fixme: really need to handle this case faster somehow
3197#if $loc eq $origloc die "<thwap> bad admin: all locations used, your network is too fragmented";
3198 }
3199 # And now we should have a unique location. tinydns fundamentally limits the
3200 # number of these but there's no doc on what characters are valid.
3201 $shdesc = $loc if !$shdesc;
3202 $dbh->do("INSERT INTO locations (location, group_id, iplist, description, comments) VALUES (?,?,?,?,?)",
3203 undef, ($loc, $grp, $iplist, $shdesc, $comments) );
[487]3204 $self->_log(entry => "Added location ($shdesc, '$iplist')");
[377]3205 $dbh->commit;
[375]3206 };
3207 if ($@) {
3208 my $msg = $@;
3209 eval { $dbh->rollback; };
[517]3210 if ($self->{log_failures}) {
[375]3211 $shdesc = $loc if !$shdesc;
[487]3212 $self->_log(entry => "Failed adding location ($shdesc, '$iplist'): $msg");
[375]3213 $dbh->commit;
3214 }
3215 return ('FAIL',$msg);
3216 }
3217
3218 return ('OK',$loc);
[377]3219} # end addLoc()
[375]3220
3221
[370]3222## DNSDB::updateLoc()
[428]3223# Update details of a location.
3224# Takes a database handle, location ID, group ID, short description,
3225# long comments/notes, and comma/space-separated IP list
3226# Returns a result code and message
[377]3227sub updateLoc {
[480]3228 my $self = shift;
3229 my $dbh = $self->{dbh};
[377]3230 my $loc = shift;
3231 my $grp = shift;
3232 my $shdesc = shift;
3233 my $comments = shift;
3234 my $iplist = shift;
[370]3235
[377]3236 $shdesc = '' if !$shdesc;
3237 $comments = '' if !$comments;
3238 $iplist = '' if !$iplist;
3239
3240 # Allow transactions, and raise an exception on errors so we can catch it later.
3241 # Use local to make sure these get "reset" properly on exiting this block
3242 local $dbh->{AutoCommit} = 0;
3243 local $dbh->{RaiseError} = 1;
3244
[480]3245 my $oldloc = $self->getLoc($loc);
[377]3246 my $okmsg = "Updated location (".$oldloc->{description}.", '".$oldloc->{iplist}."') to ($shdesc, '$iplist')";
3247
3248 eval {
3249 $dbh->do("UPDATE locations SET group_id=?,iplist=?,description=?,comments=? WHERE location=?",
3250 undef, ($grp, $iplist, $shdesc, $comments, $loc) );
[487]3251 $self->_log(entry => $okmsg);
[377]3252 $dbh->commit;
3253 };
3254 if ($@) {
3255 my $msg = $@;
3256 eval { $dbh->rollback; };
[517]3257 if ($self->{log_failures}) {
[377]3258 $shdesc = $loc if !$shdesc;
[487]3259 $self->_log(entry => "Failed updating location ($shdesc, '$iplist'): $msg");
[377]3260 $dbh->commit;
3261 }
3262 return ('FAIL',$msg);
3263 }
3264
3265 return ('OK',$okmsg);
3266} # end updateLoc()
3267
3268
[370]3269## DNSDB::delLoc()
[428]3270sub delLoc {
[480]3271 my $self = shift;
3272 my $dbh = $self->{dbh};
[428]3273 my $loc = shift;
[370]3274
[428]3275 # Allow transactions, and raise an exception on errors so we can catch it later.
3276 # Use local to make sure these get "reset" properly on exiting this block
3277 local $dbh->{AutoCommit} = 0;
3278 local $dbh->{RaiseError} = 1;
[370]3279
[480]3280 my $oldloc = $self->getLoc($loc);
[428]3281 my $olddesc = ($oldloc->{description} ? $oldloc->{description} : $loc);
3282 my $okmsg = "Deleted location ($olddesc, '".$oldloc->{iplist}."')";
3283
3284 eval {
3285 # Check for records with this location first. Deleting a location without deleting records
3286 # tagged for that location will render them unpublished without other warning.
3287 my ($r) = $dbh->selectrow_array("SELECT record_id FROM records WHERE location=? LIMIT 1", undef, ($loc) );
3288 die "Records still exist in location $olddesc\n" if $r;
3289 $dbh->do("DELETE FROM locations WHERE location=?", undef, ($loc) );
[487]3290 $self->_log(entry => $okmsg);
[428]3291 $dbh->commit;
3292 };
3293 if ($@) {
3294 my $msg = $@;
3295 eval { $dbh->rollback; };
[517]3296 if ($self->{log_failures}) {
[487]3297 $self->_log(entry => "Failed to delete location ($olddesc, '$oldloc->{iplist}'): $msg");
[428]3298 $dbh->commit;
3299 }
3300 return ('FAIL', "Failed to delete location ($olddesc, '$oldloc->{iplist}'): $msg");
3301 }
3302
3303 return ('OK',$okmsg);
3304} # end delLoc()
3305
3306
[375]3307## DNSDB::getLoc()
[428]3308# Get details about a location/view
3309# Takes a database handle and location ID.
3310# Returns a reference to a hash containing the group ID, IP list, description, and comments/notes
[375]3311sub getLoc {
[480]3312 my $self = shift;
3313 my $dbh = $self->{dbh};
[375]3314 my $loc = shift;
3315
3316 my $sth = $dbh->prepare("SELECT group_id,iplist,description,comments FROM locations WHERE location=?");
3317 $sth->execute($loc);
3318 return $sth->fetchrow_hashref();
3319} # end getLoc()
3320
3321
[370]3322## DNSDB::getLocCount()
3323# Get count of locations/views
3324# Takes a database handle and hash containing at least the current group, and optionally:
3325# - a reference list of secondary groups
3326# - a filter string
3327# - a "Starts with" string
3328sub getLocCount {
[480]3329 my $self = shift;
3330 my $dbh = $self->{dbh};
[370]3331
3332 my %args = @_;
3333
[509]3334 # Fail on bad curgroup argument. There's no sane fallback on this one.
3335 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
3336 $errstr = "Bad or missing curgroup argument";
3337 return;
3338 }
3339 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
3340 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
3341 $errstr = "Bad childlist argument";
3342 return;
3343 }
3344
[370]3345 my @filterargs;
3346 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
3347 push @filterargs, "^$args{startwith}" if $args{startwith};
3348 push @filterargs, $args{filter} if $args{filter};
3349
3350 my $sql = "SELECT count(*) FROM locations ".
3351 "WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
3352 ($args{startwith} ? " AND description ~* ?" : '').
3353 ($args{filter} ? " AND description ~* ?" : '');
3354 my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) );
3355 $errstr = $dbh->errstr if !$count;
3356 return $count;
3357} # end getLocCount()
3358
3359
3360## DNSDB::getLocList()
3361sub getLocList {
[480]3362 my $self = shift;
3363 my $dbh = $self->{dbh};
[370]3364
3365 my %args = @_;
3366
[509]3367 # Fail on bad curgroup argument. There's no sane fallback on this one.
3368 if (!$args{curgroup} || $args{curgroup} !~ /^\d+$/) {
3369 $errstr = "Bad or missing curgroup argument";
3370 return;
3371 }
3372 # Fail on bad childlist argument. This could be sanely ignored if bad, maybe.
3373 if ($args{childlist} && $args{childlist} !~ /^[\d,]+$/) {
3374 $errstr = "Bad childlist argument";
3375 return;
3376 }
3377
[370]3378 my @filterargs;
3379 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/;
3380 push @filterargs, "^$args{startwith}" if $args{startwith};
3381 push @filterargs, $args{filter} if $args{filter};
3382
3383 # better to request sorts on "simple" names, but it means we need to map it to real columns
3384# my %sortmap = (user => 'u.username', type => 'u.type', group => 'g.group_name', status => 'u.status',
3385# fname => 'fname');
3386# $args{sortby} = $sortmap{$args{sortby}};
3387
3388 # protection against bad or missing arguments
[508]3389 $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
3390 $args{sortby} = 'l.description' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
[397]3391 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
[370]3392
3393 my $sql = "SELECT l.location, l.description, l.iplist, g.group_name ".
3394 "FROM locations l ".
3395 "INNER JOIN groups g ON l.group_id=g.group_id ".
3396 "WHERE l.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")".
3397 ($args{startwith} ? " AND l.description ~* ?" : '').
3398 ($args{filter} ? " AND l.description ~* ?" : '').
3399 " ORDER BY $args{sortby} $args{sortorder} ".
[517]3400 ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
[370]3401 my $ulist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) );
3402 $errstr = $dbh->errstr if !$ulist;
3403 return $ulist;
3404} # end getLocList()
3405
3406
3407## DNSDB::getLocDropdown()
[383]3408# Get a list of location names for use in a dropdown menu.
3409# Takes a database handle, current group, and optional "tag this as selected" flag.
3410# Returns a reference to a list of hashrefs suitable to feeding to HTML::Template
3411sub getLocDropdown {
[480]3412 my $self = shift;
3413 my $dbh = $self->{dbh};
[383]3414 my $grp = shift;
3415 my $sel = shift || '';
[370]3416
[383]3417 my $sth = $dbh->prepare(qq(
3418 SELECT description,location FROM locations
3419 WHERE group_id=?
3420 ORDER BY description
3421 ) );
3422 $sth->execute($grp);
[370]3423
[383]3424 my @loclist;
[446]3425 push @loclist, { locname => "(Default/All)", loc => '', selected => ($sel ? 0 : ($sel eq '' ? 1 : 0)) };
[383]3426 while (my ($locname, $loc) = $sth->fetchrow_array) {
3427 my %row = (
3428 locname => $locname,
3429 loc => $loc,
3430 selected => ($sel eq $loc ? 1 : 0)
3431 );
3432 push @loclist, \%row;
3433 }
3434 return \@loclist;
3435} # end getLocDropdown()
3436
3437
[2]3438## DNSDB::getSOA()
3439# Return all suitable fields from an SOA record in separate elements of a hash
[224]3440# Takes a database handle, default/live flag, domain/reverse flag, and parent ID
[2]3441sub getSOA {
3442 $errstr = '';
[481]3443 my $self = shift;
3444 my $dbh = $self->{dbh};
[2]3445 my $def = shift;
[224]3446 my $rev = shift;
[2]3447 my $id = shift;
3448
[224]3449 # (ab)use distance and weight columns to store SOA data? can't for default_rev_records...
3450 # - should really attach serial to the zone parent somewhere
[101]3451
[224]3452 my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev).
3453 " WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}";
[311]3454 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
3455 return if !$ret;
[246]3456##fixme: stick a flag somewhere if the record doesn't exist. by the API, this is an impossible case, but...
[2]3457
[311]3458 ($ret->{contact},$ret->{prins}) = split /:/, $ret->{host};
3459 delete $ret->{host};
3460 ($ret->{refresh},$ret->{retry},$ret->{expire},$ret->{minttl}) = split /:/, $ret->{val};
3461 delete $ret->{val};
[2]3462
[311]3463 return $ret;
[2]3464} # end getSOA()
3465
3466
[246]3467## DNSDB::updateSOA()
3468# Update the specified SOA record
3469# Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash
[277]3470# Returns a two-element list with a result code and message
[246]3471sub updateSOA {
[473]3472 my $self = shift;
3473 my $dbh = $self->{dbh};
[246]3474 my $defrec = shift;
[248]3475 my $revrec = shift;
[246]3476
3477 my %soa = @_;
3478
[473]3479 my $oldsoa = $self->getSOA($defrec, $revrec, $soa{id});
[277]3480
[311]3481 my $msg;
3482 my %logdata;
3483 if ($defrec eq 'n') {
3484 $logdata{domain_id} = $soa{id} if $revrec eq 'n';
3485 $logdata{rdns_id} = $soa{id} if $revrec eq 'y';
[473]3486 $logdata{group_id} = $self->parentID(id => $soa{id}, revrec => $revrec,
3487 type => ($revrec eq 'n' ? 'domain' : 'revzone') );
[311]3488 } else {
3489 $logdata{group_id} = $soa{id};
3490 }
[473]3491 my $parname = ($defrec eq 'y' ? $self->groupName($soa{id}) :
3492 ($revrec eq 'n' ? $self->domainName($soa{id}) : $self->revName($soa{id})) );
[311]3493
[277]3494 # Allow transactions, and raise an exception on errors so we can catch it later.
3495 # Use local to make sure these get "reset" properly on exiting this block
3496 local $dbh->{AutoCommit} = 0;
3497 local $dbh->{RaiseError} = 1;
3498
3499 eval {
[565]3500 if (!$oldsoa) {
3501 # old SOA record is missing for some reason. create a new one.
3502 my $sql = "INSERT INTO "._rectable($defrec, $revrec)." (group_id, host, type, val, ttl) VALUES (?,?,6,?,?)";
3503 $dbh->do($sql, undef, ($soa{id}, "$soa{contact}:$soa{prins}",
3504 "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}", $soa{ttl}) );
3505 $msg = ($defrec eq 'y' ? ($revrec eq 'y' ? 'Default reverse ' : 'Default ') : '').
3506 "SOA missing for $parname; added (ns $soa{prins}, contact $soa{contact}, refresh $soa{refresh},".
3507 " retry $soa{retry}, expire $soa{expire}, minTTL $soa{minttl}, TTL $soa{ttl})";
3508 } else {
3509 my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6";
3510 $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}",
[311]3511 $soa{ttl}, $oldsoa->{record_id}) );
[565]3512 $msg = "Updated ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse ' : 'default ') : '').
[311]3513 "SOA for $parname: ".
3514 "(ns $oldsoa->{prins}, contact $oldsoa->{contact}, refresh $oldsoa->{refresh},".
3515 " retry $oldsoa->{retry}, expire $oldsoa->{expire}, minTTL $oldsoa->{minttl}, TTL $oldsoa->{ttl}) to ".
[277]3516 "(ns $soa{prins}, contact $soa{contact}, refresh $soa{refresh},".
3517 " retry $soa{retry}, expire $soa{expire}, minTTL $soa{minttl}, TTL $soa{ttl})";
[565]3518 }
[311]3519 $logdata{entry} = $msg;
[487]3520 $self->_log(%logdata);
[277]3521
3522 $dbh->commit;
3523 };
3524 if ($@) {
3525 $msg = $@;
3526 eval { $dbh->rollback; };
[311]3527 $logdata{entry} = "Error updating ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse zone ' : 'default ') : '').
3528 "SOA record for $parname: $msg";
[517]3529 if ($self->{log_failures}) {
[487]3530 $self->_log(%logdata);
[311]3531 $dbh->commit;
3532 }
3533 return ('FAIL', $logdata{entry});
[277]3534 } else {
3535 return ('OK', $msg);
3536 }
[246]3537} # end updateSOA()
3538
3539
[2]3540## DNSDB::getRecLine()
3541# Return all data fields for a zone record in separate elements of a hash
[243]3542# Takes a database handle, default/live flag, forward/reverse flag, and record ID
[2]3543sub getRecLine {
3544 $errstr = '';
[481]3545 my $self = shift;
3546 my $dbh = $self->{dbh};
[243]3547 my $defrec = shift;
3548 my $revrec = shift;
[2]3549 my $id = shift;
3550
[543]3551##fixme: do we need a knob to twist to switch between unix epoch and postgres time string?
[429]3552 my $sql = "SELECT record_id,host,type,val,ttl".
3553 ($defrec eq 'n' ? ',location' : '').
3554 ($revrec eq 'n' ? ',distance,weight,port' : '').
[543]3555 (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id,stamp,stamp < now() AS ispast,expires,stampactive FROM ').
[243]3556 _rectable($defrec,$revrec)." WHERE record_id=?";
[123]3557 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
[2]3558
[90]3559 if ($dbh->err) {
[2]3560 $errstr = $DBI::errstr;
3561 return undef;
3562 }
3563
[123]3564 if (!$ret) {
3565 $errstr = "No such record";
3566 return undef;
3567 }
3568
[243]3569 # explicitly set a parent id
3570 if ($defrec eq 'y') {
3571 $ret->{parid} = $ret->{group_id};
3572 } else {
3573 $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id});
3574 # and a secondary if we have a custom type that lives in both a forward and reverse zone
3575 $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279;
3576 }
[451]3577 $ret->{address} = $ret->{val}; # because.
[90]3578
3579 return $ret;
[2]3580}
3581
3582
3583##fixme: should use above (getRecLine()) to get lines for below?
[495]3584## DNSDB::getRecList()
3585# Return records for a group or zone
3586# Takes a default/live flag, group or zone ID, start,
[2]3587# number of records, sort field, and sort order
3588# Returns a reference to an array of hashes
[495]3589sub getRecList {
[2]3590 $errstr = '';
[481]3591 my $self = shift;
3592 my $dbh = $self->{dbh};
[2]3593
[397]3594 my %args = @_;
[4]3595
[397]3596 my @filterargs;
[135]3597
[397]3598 push @filterargs, $args{filter} if $args{filter};
3599
3600 # protection against bad or missing arguments
[508]3601 $args{sortorder} = 'ASC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
[510]3602 my $defsort;
3603 $defsort = 'host' if $args{revrec} eq 'n'; # default sort by host on domain record list
3604 $defsort = 'val' if $args{revrec} eq 'y'; # default sort by IP on revzone record list
[513]3605 $args{sortby} = '' if !$args{sortby};
[508]3606 $args{sortby} = $defsort if !$args{revrec};
[512]3607 $args{sortby} = $defsort if $args{sortby} !~ /^[\w_,.]+$/;
[397]3608 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
[517]3609 my $perpage = ($args{nrecs} ? $args{nrecs} : $self->{perpage});
[397]3610
[352]3611 # sort reverse zones on IP, correctly
[397]3612 # do other fiddling with $args{sortby} while we're at it.
[420]3613 # whee! multisort means just passing comma-separated fields in sortby!
3614 my $newsort = '';
3615 foreach my $sf (split /,/, $args{sortby}) {
3616 $sf = "r.$sf";
3617 $sf =~ s/r\.val/CAST (r.val AS inet)/
3618 if $args{revrec} eq 'y' && $args{defrec} eq 'n';
3619 $sf =~ s/r\.type/t.alphaorder/;
3620 $newsort .= ",$sf";
3621 }
3622 $newsort =~ s/^,//;
[352]3623
[543]3624##fixme: do we need a knob to twist to switch from unix epoch to postgres time string?
[224]3625 my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl";
[543]3626 $sql .= ",l.description AS locname,stamp,r.stamp < now() AS ispast,r.expires,r.stampactive"
3627 if $args{defrec} eq 'n';
[397]3628 $sql .= ",r.distance,r.weight,r.port" if $args{revrec} eq 'n';
3629 $sql .= " FROM "._rectable($args{defrec},$args{revrec})." r ";
[104]3630 $sql .= "INNER JOIN rectypes t ON r.type=t.val "; # for sorting by type alphabetically
[397]3631 $sql .= "LEFT JOIN locations l ON r.location=l.location " if $args{defrec} eq 'n';
3632 $sql .= "WHERE "._recparent($args{defrec},$args{revrec})." = ?";
[104]3633 $sql .= " AND NOT r.type=$reverse_typemap{SOA}";
[439]3634 $sql .= " AND (r.host ~* ? OR r.val ~* ?)" if $args{filter};
[420]3635 $sql .= " ORDER BY $newsort $args{sortorder}";
[324]3636 # ensure consistent ordering by sorting on record_id too
[397]3637 $sql .= ", record_id $args{sortorder}";
[501]3638 $sql .= ($args{offset} eq 'all' ? '' : " LIMIT $perpage OFFSET ".$args{offset}*$perpage);
[4]3639
[397]3640 my @bindvars = ($args{id});
[439]3641 push @bindvars, ($args{filter},$args{filter}) if $args{filter};
[224]3642
[397]3643 my $ret = $dbh->selectall_arrayref($sql, { Slice => {} }, (@bindvars) );
[495]3644 $errstr = "Error retrieving records: ".$dbh->errstr if !$ret;
[543]3645
[2]3646 return $ret;
[495]3647} # end getRecList()
[2]3648
3649
[91]3650## DNSDB::getRecCount()
[224]3651# Return count of non-SOA records in zone (or default records in a group)
3652# Takes a database handle, default/live flag, reverse/forward flag, group/domain ID,
3653# and optional filtering modifier
[91]3654# Returns the count
3655sub getRecCount {
[481]3656 my $self = shift;
3657 my $dbh = $self->{dbh};
[91]3658 my $defrec = shift;
[224]3659 my $revrec = shift;
[91]3660 my $id = shift;
[135]3661 my $filter = shift || '';
[91]3662
[135]3663 # keep the nasties down, since we can't ?-sub this bit. :/
3664 # note this is chars allowed in DNS hostnames
3665 $filter =~ s/[^a-zA-Z0-9_.:-]//g;
3666
[222]3667 my @bindvars = ($id);
3668 push @bindvars, $filter if $filter;
[224]3669 my $sql = "SELECT count(*) FROM ".
3670 _rectable($defrec,$revrec).
3671 " WHERE "._recparent($defrec,$revrec)."=? ".
3672 "AND NOT type=$reverse_typemap{SOA}".
3673 ($filter ? " AND host ~* ?" : '');
3674 my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) );
[91]3675
3676 return $count;
3677
3678} # end getRecCount()
3679
3680
[3]3681## DNSDB::addRec()
[2]3682# Add a new record to a domain or a group's default records
3683# Takes a database handle, default/live flag, group/domain ID,
3684# host, type, value, and TTL
3685# Some types require additional detail: "distance" for MX and SRV,
3686# and weight/port for SRV
3687# Returns a status code and detail message in case of error
[234]3688##fixme: pass a hash with the record data, not a series of separate values
[2]3689sub addRec {
3690 $errstr = '';
[481]3691 my $self = shift;
3692 my $dbh = $self->{dbh};
[2]3693 my $defrec = shift;
[226]3694 my $revrec = shift;
3695 my $id = shift; # parent (group_id for defrecs, rdns_id for reverse records,
3696 # domain_id for domain records)
[2]3697
3698 my $host = shift;
[234]3699 my $rectype = shift; # reference so we can coerce it if "+"-types can't find both zones
[2]3700 my $val = shift;
3701 my $ttl = shift;
[389]3702 my $location = shift;
3703 $location = '' if !$location;
[2]3704
[543]3705 my $expires = shift;
3706 $expires = 1 if $expires eq 'until'; # Turn some special values into the appropriate booleans.
3707 $expires = 0 if $expires eq 'after';
3708 my $stamp = shift;
3709 $stamp = '' if !$stamp; # Timestamp should be a string at this point.
3710
[417]3711 # Spaces are evil.
3712 $$host =~ s/^\s+//;
3713 $$host =~ s/\s+$//;
[418]3714 if ($typemap{$$rectype} ne 'TXT') {
[417]3715 # Leading or trailng spaces could be legit in TXT records.
3716 $$val =~ s/^\s+//;
3717 $$val =~ s/\s+$//;
3718 }
3719
[542]3720 if ($self->{lowercase}) {
3721 if ($typemap{$$rectype} ne 'TXT') {
3722 $$host = lc($$host);
3723 $$val = lc($$val);
3724 } else {
3725 # TXT records should preserve user entry in the string.
3726 if ($revrec eq 'n') {
3727 $$host = lc($$host);
3728 } else {
3729 $$val = lc($$val);
3730 }
3731 }
3732 }
3733
[226]3734 # prep for validation
[252]3735 my $addr = NetAddr::IP->new($$val);
3736 $$host =~ s/\.+$//; # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
[226]3737
3738 my $domid = 0;
3739 my $revid = 0;
3740
3741 my $retcode = 'OK'; # assume everything will go OK
3742 my $retmsg = '';
3743
3744 # do simple validation first
[559]3745 return ('FAIL', "TTL must be numeric") unless $ttl =~ /^-?\d+$/;
[226]3746
[532]3747 # Quick check on hostname parts. There are enough variations to justify a sub now.
3748 return ('FAIL', $errstr) if ! _check_hostname_form($$host, $$rectype, $defrec, $revrec);
[226]3749
[234]3750 # Collect these even if we're only doing a simple A record so we can call *any* validation sub
3751 my $dist = shift;
[281]3752 my $weight = shift;
[234]3753 my $port = shift;
[226]3754
[234]3755 my $fields;
3756 my @vallist;
[226]3757
[234]3758 # Call the validation sub for the type requested.
[481]3759 ($retcode,$retmsg) = $validators{$$rectype}($self, defrec => $defrec, revrec => $revrec, id => $id,
[249]3760 host => $host, rectype => $rectype, val => $val, addr => $addr,
[234]3761 dist => \$dist, port => \$port, weight => \$weight,
[481]3762 fields => \$fields, vallist => \@vallist);
[129]3763
[234]3764 return ($retcode,$retmsg) if $retcode eq 'FAIL';
[209]3765
[234]3766 # Set up database fields and bind parameters
[481]3767 $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec);
3768 push @vallist, ($$host,$$rectype,$$val,$ttl,$id);
3769
3770 if ($defrec eq 'n') {
[543]3771 # locations are not for default records, silly coder!
[481]3772 $fields .= ",location";
3773 push @vallist, $location;
[543]3774 # timestamps are rare.
3775 if ($stamp) {
3776 $fields .= ",stamp,expires,stampactive";
3777 push @vallist, $stamp, $expires, 'y';
3778 } else {
3779 $fields .= ",stampactive";
3780 push @vallist, 'n';
3781 }
[481]3782 }
[543]3783
3784 # a little magic to get the right number of ? placeholders based on how many values we're providing
[234]3785 my $vallen = '?'.(',?'x$#vallist);
[2]3786
[287]3787 # Put together the success log entry. We have to use this horrible kludge
3788 # because domain_id and rdns_id may or may not be present, and if they are,
3789 # they're not at a guaranteed consistent index in the array. wheee!
3790 my %logdata;
3791 my @ftmp = split /,/, $fields;
3792 for (my $i=0; $i <= $#vallist; $i++) {
3793 $logdata{domain_id} = $vallist[$i] if $ftmp[$i] eq 'domain_id';
3794 $logdata{rdns_id} = $vallist[$i] if $ftmp[$i] eq 'rdns_id';
3795 }
3796 $logdata{group_id} = $id if $defrec eq 'y';
[473]3797 $logdata{group_id} = $self->parentID(id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec)
[287]3798 if $defrec eq 'n';
[328]3799 $logdata{entry} = "Added ".($defrec eq 'y' ? 'default record' : 'record');
3800 # NS records for revzones get special treatment
3801 if ($revrec eq 'y' && $$rectype == 2) {
3802 $logdata{entry} .= " '$$val $typemap{$$rectype} $$host";
3803 } else {
3804 $logdata{entry} .= " '$$host $typemap{$$rectype} $$val";
3805 }
3806
[287]3807 $logdata{entry} .= " [distance $dist]" if $typemap{$$rectype} eq 'MX';
3808 $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]"
3809 if $typemap{$$rectype} eq 'SRV';
[425]3810 $logdata{entry} .= "', TTL $ttl";
[480]3811 $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
[543]3812 $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
[287]3813
[90]3814 # Allow transactions, and raise an exception on errors so we can catch it later.
3815 # Use local to make sure these get "reset" properly on exiting this block
3816 local $dbh->{AutoCommit} = 0;
3817 local $dbh->{RaiseError} = 1;
[2]3818
[90]3819 eval {
[236]3820 $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)",
[90]3821 undef, @vallist);
[487]3822 $self->_log(%logdata);
[90]3823 $dbh->commit;
3824 };
3825 if ($@) {
3826 my $msg = $@;
3827 eval { $dbh->rollback; };
[517]3828 if ($self->{log_failures}) {
[289]3829 $logdata{entry} = "Failed adding ".($defrec eq 'y' ? 'default ' : '').
3830 "record '$$host $typemap{$$rectype} $$val', TTL $ttl ($msg)";
[487]3831 $self->_log(%logdata);
[289]3832 $dbh->commit;
3833 }
[90]3834 return ('FAIL',$msg);
3835 }
[2]3836
[287]3837 $resultstr = $logdata{entry};
[226]3838 return ($retcode, $retmsg);
[90]3839
[2]3840} # end addRec()
3841
3842
[16]3843## DNSDB::updateRec()
3844# Update a record
[273]3845# Takes a database handle, default and reverse flags, record ID, immediate parent ID, and new record data.
3846# Returns a status code and message
[16]3847sub updateRec {
3848 $errstr = '';
[17]3849
[481]3850 my $self = shift;
3851 my $dbh = $self->{dbh};
[16]3852 my $defrec = shift;
[272]3853 my $revrec = shift;
[16]3854 my $id = shift;
[272]3855 my $parid = shift; # immediate parent entity that we're descending from to update the record
[16]3856
[273]3857 # all records have these
[16]3858 my $host = shift;
[272]3859 my $hostbk = $$host; # Keep a backup copy of the original, so we can WARN if the update mangles the domain
3860 my $rectype = shift;
[16]3861 my $val = shift;
3862 my $ttl = shift;
[389]3863 my $location = shift; # may be empty/null/undef depending on caller
3864 $location = '' if !$location;
[16]3865
[543]3866 my $expires = shift;
3867 $expires = 1 if $expires eq 'until'; # Turn some special values into the appropriate booleans.
3868 $expires = 0 if $expires eq 'after';
3869 my $stamp = shift;
3870 $stamp = '' if !$stamp; # Timestamp should be a string at this point.
3871
[510]3872 # just set it to an empty string; failures will be caught later.
3873 $$host = '' if !$$host;
3874
[417]3875 # Spaces are evil.
3876 $$host =~ s/^\s+//;
3877 $$host =~ s/\s+$//;
[418]3878 if ($typemap{$$rectype} ne 'TXT') {
[417]3879 # Leading or trailng spaces could be legit in TXT records.
3880 $$val =~ s/^\s+//;
3881 $$val =~ s/\s+$//;
3882 }
3883
[542]3884 if ($self->{lowercase}) {
3885 if ($typemap{$$rectype} ne 'TXT') {
3886 $$host = lc($$host);
3887 $$val = lc($$val);
3888 } else {
3889 # TXT records should preserve user entry in the string.
3890 if ($revrec eq 'n') {
3891 $$host = lc($$host);
3892 } else {
3893 $$val = lc($$val);
3894 }
3895 }
3896 }
3897
[272]3898 # prep for validation
3899 my $addr = NetAddr::IP->new($$val);
3900 $$host =~ s/\.+$//; # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
[16]3901
[272]3902 my $domid = 0;
3903 my $revid = 0;
3904
3905 my $retcode = 'OK'; # assume everything will go OK
3906 my $retmsg = '';
3907
[273]3908 # do simple validation first
[559]3909 return ('FAIL', "TTL must be numeric") unless $ttl =~ /^-?\d+$/;
[272]3910
[532]3911 # Quick check on hostname parts. There are enough variations to justify a sub now.
3912 return ('FAIL', $errstr) if ! _check_hostname_form($$host, $$rectype, $defrec, $revrec);
[272]3913
[273]3914 # only MX and SRV will use these
[288]3915 my $dist = shift || 0;
3916 my $weight = shift || 0;
3917 my $port = shift || 0;
[16]3918
[272]3919 my $fields;
3920 my @vallist;
[16]3921
[273]3922 # get old record data so we have the right parent ID
3923 # and for logging (eventually)
[481]3924 my $oldrec = $self->getRecLine($defrec, $revrec, $id);
[223]3925
[272]3926 # Call the validation sub for the type requested.
3927 # Note the ID to pass here is the *parent*, not the record
[481]3928 ($retcode,$retmsg) = $validators{$$rectype}($self, defrec => $defrec, revrec => $revrec,
[272]3929 id => ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})),
3930 host => $host, rectype => $rectype, val => $val, addr => $addr,
3931 dist => \$dist, port => \$port, weight => \$weight,
3932 fields => \$fields, vallist => \@vallist,
[481]3933 update => $id);
[272]3934
3935 return ($retcode,$retmsg) if $retcode eq 'FAIL';
3936
[273]3937 # Set up database fields and bind parameters. Note only the optional fields
3938 # (distance, weight, port, secondary parent ID) are added in the validation call above
[448]3939 $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec);
3940 push @vallist, ($$host,$$rectype,$$val,$ttl,
[273]3941 ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})) );
[272]3942
[448]3943 if ($defrec eq 'n') {
[543]3944 # locations are not for default records, silly coder!
[448]3945 $fields .= ",location";
3946 push @vallist, $location;
[543]3947 # timestamps are rare.
3948 if ($stamp) {
3949 $fields .= ",stamp,expires,stampactive";
3950 push @vallist, $stamp, $expires, 'y';
3951 } else {
3952 $fields .= ",stampactive";
3953 push @vallist, 'n';
3954 }
[448]3955 }
3956
[273]3957 # hack hack PTHUI
3958 # need to forcibly make sure we disassociate a record with a parent it's no longer related to.
3959 # eg, PTR records may not have a domain parent, or A/AAAA records may not have a revzone parent.
3960 # mainly needed for crossover types that got coerced down to "standard" types
3961 if ($defrec eq 'n') {
3962 if ($$rectype == $reverse_typemap{PTR}) {
3963 $fields .= ",domain_id";
3964 push @vallist, 0;
3965 }
3966 if ($$rectype == $reverse_typemap{A} || $$rectype == $reverse_typemap{AAAA}) {
3967 $fields .= ",rdns_id";
3968 push @vallist, 0;
3969 }
3970 }
[345]3971 # fix fat-finger-originated record type changes
3972 if ($$rectype == 65285) {
3973 $fields .= ",rdns_id" if $revrec eq 'n';
3974 $fields .= ",domain_id" if $revrec eq 'y';
3975 push @vallist, 0;
3976 }
[341]3977 if ($defrec eq 'n') {
3978 $domid = $parid if $revrec eq 'n';
3979 $revid = $parid if $revrec eq 'y';
3980 }
[272]3981
[288]3982 # Put together the success log entry. Horrible kludge from addRec() copied as-is since
3983 # we don't know whether the passed arguments or retrieved values for domain_id and rdns_id
3984 # will be maintained (due to "not-in-zone" validation changes)
3985 my %logdata;
[341]3986 $logdata{domain_id} = $domid;
3987 $logdata{rdns_id} = $revid;
[288]3988 my @ftmp = split /,/, $fields;
3989 for (my $i=0; $i <= $#vallist; $i++) {
3990 $logdata{domain_id} = $vallist[$i] if $ftmp[$i] eq 'domain_id';
3991 $logdata{rdns_id} = $vallist[$i] if $ftmp[$i] eq 'rdns_id';
3992 }
3993 $logdata{group_id} = $parid if $defrec eq 'y';
[473]3994 $logdata{group_id} = $self->parentID(id => $parid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec)
[288]3995 if $defrec eq 'n';
[328]3996 $logdata{entry} = "Updated ".($defrec eq 'y' ? 'default record' : 'record')." from\n";
3997 # NS records for revzones get special treatment
3998 if ($revrec eq 'y' && $$rectype == 2) {
3999 $logdata{entry} .= " '$oldrec->{val} $typemap{$oldrec->{type}} $oldrec->{host}";
4000 } else {
4001 $logdata{entry} .= " '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}";
4002 }
[288]4003 $logdata{entry} .= " [distance $oldrec->{distance}]" if $typemap{$oldrec->{type}} eq 'MX';
4004 $logdata{entry} .= " [priority $oldrec->{distance}] [weight $oldrec->{weight}] [port $oldrec->{port}]"
4005 if $typemap{$oldrec->{type}} eq 'SRV';
[425]4006 $logdata{entry} .= "', TTL $oldrec->{ttl}";
[480]4007 $logdata{entry} .= ", location ".$self->getLoc($oldrec->{location})->{description} if $oldrec->{location};
[543]4008 $logdata{entry} .= ($oldrec->{expires} ? ', expires at ' : ', valid after ').$oldrec->{stamp}
4009 if $oldrec->{stampactive};
[425]4010 $logdata{entry} .= "\nto\n";
[328]4011 # More NS special
4012 if ($revrec eq 'y' && $$rectype == 2) {
4013 $logdata{entry} .= "'$$val $typemap{$$rectype} $$host";
4014 } else {
4015 $logdata{entry} .= "'$$host $typemap{$$rectype} $$val";
4016 }
[288]4017 $logdata{entry} .= " [distance $dist]" if $typemap{$$rectype} eq 'MX';
4018 $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$$rectype} eq 'SRV';
[425]4019 $logdata{entry} .= "', TTL $ttl";
[480]4020 $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
[543]4021 $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
[288]4022
[90]4023 local $dbh->{AutoCommit} = 0;
4024 local $dbh->{RaiseError} = 1;
4025
[341]4026 # Fiddle the field list into something suitable for updates
4027 $fields =~ s/,/=?,/g;
4028 $fields .= "=?";
4029
[90]4030 eval {
[273]4031 $dbh->do("UPDATE "._rectable($defrec,$revrec)." SET $fields WHERE record_id=?", undef, (@vallist, $id) );
[487]4032 $self->_log(%logdata);
[130]4033 $dbh->commit;
[90]4034 };
4035 if ($@) {
4036 my $msg = $@;
[288]4037 eval { $dbh->rollback; };
[517]4038 if ($self->{log_failures}) {
[289]4039 $logdata{entry} = "Failed updating ".($defrec eq 'y' ? 'default ' : '').
4040 "record '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)";
[487]4041 $self->_log(%logdata);
[289]4042 $dbh->commit;
4043 }
[90]4044 return ('FAIL', $msg);
4045 }
4046
[288]4047 $resultstr = $logdata{entry};
[272]4048 return ($retcode, $retmsg);
[16]4049} # end updateRec()
4050
4051
[459]4052## DNSDB::downconvert()
4053# A mostly internal (not exported) semiutilty sub to downconvert from pseudotype <x>
4054# to a compatible component type. Only a handful of operations are valid, anything
4055# else is a null-op.
4056# Takes the record ID and the new type. Returns boolean.
4057sub downconvert {
[481]4058 my $self = shift;
4059 my $dbh = $self->{dbh};
[459]4060 my $recid = shift;
4061 my $newtype = shift;
4062
4063 # also, only work on live records; little to no value trying to do this on default records.
[481]4064 my $rec = $self->getRecLine('n', 'y', $recid);
[459]4065
4066 # hm?
4067 #return 1 if !$rec;
4068
4069 return 1 if $rec->{type} < 65000; # Only the reverse-record pseudotypes can be downconverted
4070 return 1 if $rec->{type} == 65282; # Nowhere to go
4071
4072 my $delpar;
4073 my @sqlargs;
4074 if ($rec->{type} == 65280) {
4075 return 1 if $newtype != 1 && $newtype != 12;
4076 $delpar = ($newtype == 1 ? 'rdns_id' : 'domain_id');
4077 push @sqlargs, 0, $newtype, $recid;
4078 } elsif ($rec->{type} == 65281) {
4079 return 1 if $newtype != 28 && $newtype != 12;
4080 $delpar = ($newtype == 28 ? 'rdns_id' : 'domain_id');
4081 push @sqlargs, 0, $newtype, $recid;
4082 } elsif ($rec->{type} == 65283) {
4083 return 1 if $newtype != 65282;
4084 $delpar = 'rdns_id';
4085 } elsif ($rec->{type} == 65284) {
4086 return 1 if $newtype != 65282;
4087 $delpar = 'rdns_id';
4088 } else {
4089 # Your llama is on fire.
4090 }
4091
4092 local $dbh->{AutoCommit} = 0;
4093 local $dbh->{RaiseError} = 1;
4094
4095 eval {
4096 $dbh->do("UPDATE records SET $delpar = ?, type = ? WHERE record_id = ?", undef, @sqlargs);
4097 $dbh->commit;
4098 };
4099 if ($@) {
4100 $errstr = $@;
4101 eval { $dbh->rollback; };
4102 return 0;
4103 }
4104 return 1;
4105} # end downconvert()
4106
4107
[3]4108## DNSDB::delRec()
4109# Delete a record.
4110sub delRec {
4111 $errstr = '';
[481]4112 my $self = shift;
4113 my $dbh = $self->{dbh};
[3]4114 my $defrec = shift;
[243]4115 my $revrec = shift;
[3]4116 my $id = shift;
4117
[481]4118 my $oldrec = $self->getRecLine($defrec, $revrec, $id);
[3]4119
[290]4120 # Allow transactions, and raise an exception on errors so we can catch it later.
4121 # Use local to make sure these get "reset" properly on exiting this block
4122 local $dbh->{AutoCommit} = 0;
4123 local $dbh->{RaiseError} = 1;
[3]4124
[290]4125 # Put together the log entry
4126 my %logdata;
4127 $logdata{domain_id} = $oldrec->{domain_id};
4128 $logdata{rdns_id} = $oldrec->{rdns_id};
4129 $logdata{group_id} = $oldrec->{group_id} if $defrec eq 'y';
[473]4130 $logdata{group_id} = $self->parentID(id => $oldrec->{domain_id}, type => ($revrec eq 'n' ? 'domain' : 'revzone'),
4131 revrec => $revrec)
[290]4132 if $defrec eq 'n';
4133 $logdata{entry} = "Deleted ".($defrec eq 'y' ? 'default record ' : 'record ').
4134 "'$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}";
4135 $logdata{entry} .= " [distance $oldrec->{distance}]" if $typemap{$oldrec->{type}} eq 'MX';
4136 $logdata{entry} .= " [priority $oldrec->{distance}] [weight $oldrec->{weight}] [port $oldrec->{port}]"
4137 if $typemap{$oldrec->{type}} eq 'SRV';
[425]4138 $logdata{entry} .= "', TTL $oldrec->{ttl}";
[480]4139 $logdata{entry} .= ", location ".$self->getLoc($oldrec->{location})->{description} if $oldrec->{location};
[290]4140
4141 eval {
4142 my $sth = $dbh->do("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?", undef, ($id));
[487]4143 $self->_log(%logdata);
[290]4144 $dbh->commit;
4145 };
4146 if ($@) {
4147 my $msg = $@;
4148 eval { $dbh->rollback; };
[517]4149 if ($self->{log_failures}) {
[290]4150 $logdata{entry} = "Error deleting ".($defrec eq 'y' ? 'default record' : 'record').
4151 " '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)";
[487]4152 $self->_log(%logdata);
[290]4153 $dbh->commit;
4154 }
4155 return ('FAIL', $msg);
4156 }
4157
4158 return ('OK',$logdata{entry});
[3]4159} # end delRec()
4160
4161
[323]4162## DNSDB::getLogCount()
4163# Get a count of log entries
4164# Takes a database handle and a hash containing at least:
4165# - Entity ID and entity type as the primary log "slice"
4166sub getLogCount {
[483]4167 my $self = shift;
4168 my $dbh = $self->{dbh};
[323]4169
4170 my %args = @_;
4171
4172 my @filterargs;
4173##fixme: which fields do we want to filter on?
4174# push @filterargs,
4175
4176 $errstr = 'Missing primary parent ID and/or type';
4177 # fail early if we don't have a "prime" ID to look for log entries for
4178 return if !$args{id};
4179
4180 # or if the prime id type is missing or invalid
4181 return if !$args{logtype};
4182 $args{logtype} = 'revzone' if $args{logtype} eq 'rdns'; # hack pthui
4183 $args{logtype} = 'domain' if $args{logtype} eq 'dom'; # hack pthui
4184 return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user');
4185
4186 my $sql = "SELECT count(*) FROM log ".
4187 "WHERE $id_col{$args{logtype}}=?".
4188 ($args{filter} ? " AND entry ~* ?" : '');
4189 my ($count) = $dbh->selectrow_array($sql, undef, ($args{id}, @filterargs) );
4190 $errstr = $dbh->errstr if !$count;
4191 return $count;
4192} # end getLogCount()
4193
4194
4195## DNSDB::getLogEntries()
4196# Get a list of log entries
4197# Takes arguments as with getLogCount() above, plus optional:
4198# - sort field
4199# - sort order
4200# - offset for pagination
4201sub getLogEntries {
[483]4202 my $self = shift;
4203 my $dbh = $self->{dbh};
[323]4204
4205 my %args = @_;
4206
4207 my @filterargs;
4208
4209 # fail early if we don't have a "prime" ID to look for log entries for
4210 return if !$args{id};
4211
4212 # or if the prime id type is missing or invalid
4213 return if !$args{logtype};
4214 $args{logtype} = 'revzone' if $args{logtype} eq 'rdns'; # hack pthui
4215 $args{logtype} = 'domain' if $args{logtype} eq 'dom'; # hack pthui
4216 return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user');
4217
4218 # Sorting defaults
[508]4219 $args{sortorder} = 'DESC' if !$args{sortorder} || !grep /^$args{sortorder}$/, ('ASC','DESC');
4220 $args{sortby} = 'stamp' if !$args{sortby} || $args{sortby} !~ /^[\w_.]+$/;
[397]4221 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/;
[323]4222
[324]4223 my %sortmap = (fname => 'name', username => 'email', entry => 'entry', stamp => 'stamp');
[323]4224 $args{sortby} = $sortmap{$args{sortby}};
4225
4226 my $sql = "SELECT user_id AS userid, email AS useremail, name AS userfname, entry AS logentry, ".
4227 "date_trunc('second',stamp) AS logtime ".
4228 "FROM log ".
4229 "WHERE $id_col{$args{logtype}}=?".
4230 ($args{filter} ? " AND entry ~* ?" : '').
[324]4231 " ORDER BY $args{sortby} $args{sortorder}, log_id $args{sortorder}".
[517]4232 ($args{offset} eq 'all' ? '' : " LIMIT $self->{perpage} OFFSET ".$args{offset}*$self->{perpage});
[323]4233 my $loglist = $dbh->selectall_arrayref($sql, { Slice => {} }, ($args{id}, @filterargs) );
4234 $errstr = $dbh->errstr if !$loglist;
4235 return $loglist;
4236} # end getLogEntries()
4237
4238
[452]4239## IPDB::getRevPattern()
4240# Get the narrowest template pattern applicable to a passed CIDR address (may be a netblock or an IP)
4241sub getRevPattern {
[483]4242 my $self = shift;
4243 my $dbh = $self->{dbh};
[452]4244 my $cidr = shift;
4245 my $group = shift || 1; # just in case
4246
[462]4247 # for speed! Casting and comparing even ~7K records takes ~2.5s, so narrow it down to one revzone first.
4248 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >>= ? AND group_id = ?",
4249 undef, ($cidr, $group) );
4250
4251##fixme? may need to narrow things down more by octet-chopping and doing text comparisons before casting.
4252 my ($revpatt) = $dbh->selectrow_array("SELECT host FROM records ".
4253 "WHERE (type in (12,65280,65281,65282,65283,65284)) AND rdns_id = ? AND CAST (val AS inet) >>= ? ".
4254 "ORDER BY CAST (val AS inet) DESC LIMIT 1", undef, ($revid, $cidr) );
[452]4255 return $revpatt;
4256} # end getRevPattern()
4257
4258
[225]4259## DNSDB::getTypelist()
4260# Get a list of record types for various UI dropdowns
4261# Takes database handle, forward/reverse/lookup flag, and optional "tag as selected" indicator (defaults to A)
4262# Returns an arrayref to list of hashrefs perfect for HTML::Template
4263sub getTypelist {
[483]4264 my $self = shift;
4265 my $dbh = $self->{dbh};
[225]4266 my $recgroup = shift;
4267 my $type = shift || $reverse_typemap{A};
4268
4269 # also accepting $webvar{revrec}!
4270 $recgroup = 'f' if $recgroup eq 'n';
4271 $recgroup = 'r' if $recgroup eq 'y';
4272
4273 my $sql = "SELECT val,name FROM rectypes WHERE ";
4274 if ($recgroup eq 'r') {
4275 # reverse zone types
4276 $sql .= "stdflag=2 OR stdflag=3";
4277 } elsif ($recgroup eq 'l') {
4278 # DNS lookup types. Note we avoid our custom types >= 65280, since those are entirely internal.
4279 $sql .= "(stdflag=1 OR stdflag=2 OR stdflag=3) AND val < 65280";
4280 } else {
4281 # default; forward zone types. technically $type eq 'f' but not worth the error message.
4282 $sql .= "stdflag=1 OR stdflag=2";
[506]4283 $sql .= " AND val < 65280" if $recgroup eq 'fo'; # An extra flag to trim off the pseudotypes as well.
[225]4284 }
4285 $sql .= " ORDER BY listorder";
4286
4287 my $sth = $dbh->prepare($sql);
4288 $sth->execute;
4289 my @typelist;
4290 while (my ($rval,$rname) = $sth->fetchrow_array()) {
4291 my %row = ( recval => $rval, recname => $rname );
4292 $row{tselect} = 1 if $rval == $type;
4293 push @typelist, \%row;
4294 }
4295
4296 # Add SOA on lookups since it's not listed in other dropdowns.
4297 if ($recgroup eq 'l') {
4298 my %row = ( recval => $reverse_typemap{SOA}, recname => 'SOA' );
4299 $row{tselect} = 1 if $reverse_typemap{SOA} == $type;
4300 push @typelist, \%row;
4301 }
4302
4303 return \@typelist;
4304} # end getTypelist()
4305
4306
[254]4307## DNSDB::parentID()
4308# Get ID of entity that is nearest parent to requested id
4309# Takes a database handle and a hash of entity ID, entity type, optional parent type flag
4310# (domain/reverse zone or group), and optional default/live and forward/reverse flags
4311# Returns the ID or undef on failure
4312sub parentID {
[473]4313 my $self = shift;
4314 my $dbh = $self->{dbh};
[116]4315
[254]4316 my %args = @_;
[116]4317
[254]4318 # clean up the parent-type. Set it to group if not set; coerce revzone to domain for simpler logic
4319 $args{partype} = 'group' if !$args{partype};
4320 $args{partype} = 'domain' if $args{partype} eq 'revzone';
[116]4321
[254]4322 # clean up defrec and revrec. default to live record, forward zone
4323 $args{defrec} = 'n' if !$args{defrec};
4324 $args{revrec} = 'n' if !$args{revrec};
[116]4325
[254]4326 if ($par_type{$args{partype}} eq 'domain') {
4327 # only live records can have a domain/zone parent
4328 return unless ($args{type} eq 'record' && $args{defrec} eq 'n');
4329 my $result = $dbh->selectrow_hashref("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id').
4330 " FROM records WHERE record_id = ?",
4331 undef, ($args{id}) ) or return;
4332 return $result;
4333 } else {
4334 # snag some arguments that will either fall through or be overwritten to save some code duplication
4335 my $tmpid = $args{id};
4336 my $type = $args{type};
4337 if ($type eq 'record' && $args{defrec} eq 'n') {
4338 # Live records go through the records table first.
4339 ($tmpid) = $dbh->selectrow_array("SELECT ".($args{revrec} eq 'n' ? 'domain_id' : 'rdns_id').
4340 " FROM records WHERE record_id = ?",
4341 undef, ($args{id}) ) or return;
4342 $type = ($args{revrec} eq 'n' ? 'domain' : 'revzone');
4343 }
4344 my ($result) = $dbh->selectrow_array("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?",
4345 undef, ($tmpid) );
4346 return $result;
4347 }
4348# should be impossible to get here with even remotely sane arguments
4349 return;
4350} # end parentID()
[116]4351
4352
[117]4353## DNSDB::isParent()
4354# Returns true if $id1 is a parent of $id2, false otherwise
4355sub isParent {
[470]4356 my $self = shift;
4357 my $dbh = $self->{dbh};
[117]4358 my $id1 = shift;
4359 my $type1 = shift;
4360 my $id2 = shift;
4361 my $type2 = shift;
4362##todo: immediate, secondary, full (default)
4363
[157]4364 # Return false on invalid types
[244]4365 return 0 if !grep /^$type1$/, ('record','defrec','defrevrec','user','domain','revzone','group');
4366 return 0 if !grep /^$type2$/, ('record','defrec','defrevrec','user','domain','revzone','group');
[157]4367
[117]4368 # Return false on impossible relations
4369 return 0 if $type1 eq 'record'; # nothing may be a child of a record
4370 return 0 if $type1 eq 'defrec'; # nothing may be a child of a record
[244]4371 return 0 if $type1 eq 'defrevrec'; # nothing may be a child of a record
[117]4372 return 0 if $type1 eq 'user'; # nothing may be child of a user
4373 return 0 if $type1 eq 'domain' && $type2 ne 'record'; # domain may not be a parent of anything other than a record
[244]4374 return 0 if $type1 eq 'revzone' && $type2 ne 'record';# reverse zone may not be a parent of anything other than a record
[117]4375
[186]4376 # ennnhhhh.... if we're passed an id of 0, it will never be found. usual
4377 # case would be the UI creating a new <thing>, and so we don't have an ID for
4378 # <thing> to look up yet. in that case the UI should check the parent as well.
4379 return 0 if $id1 == 0; # nothing can have a parent id of 0
4380 return 1 if $id2 == 0; # anything could have a child id of 0 (or "unknown")
4381
[117]4382 # group 1 is the ultimate root parent
4383 return 1 if $type1 eq 'group' && $id1 == 1;
4384
[155]4385 # groups are always (a) parent of themselves
4386 return 1 if $type1 eq 'group' && $type2 eq 'group' && $id1 == $id2;
4387
[117]4388 my $id = $id2;
4389 my $type = $type2;
4390 my $foundparent = 0;
[155]4391
[244]4392 # Records are the only entity with two possible parents. We need to split the parent checks on
4393 # domain/rdns.
4394 if ($type eq 'record') {
4395 my ($dom,$rdns) = $dbh->selectrow_array("SELECT domain_id,rdns_id FROM records WHERE record_id=?",
4396 undef, ($id));
4397 # check immediate parent against request
4398 return 1 if $type1 eq 'domain' && $id1 == $dom;
4399 return 1 if $type1 eq 'revzone' && $id1 == $rdns;
4400 # if request is group, check *both* parents. Only check if the parent is nonzero though.
[470]4401 return 1 if $dom && $self->isParent($id1, $type1, $dom, 'domain');
4402 return 1 if $rdns && $self->isParent($id1, $type1, $rdns, 'revzone');
[244]4403 # exit here since we've executed the loop below by proxy in the above recursive calls.
4404 return 0;
4405 }
4406
4407# almost the same loop as getParents() above
[186]4408 my $limiter = 0;
[117]4409 while (1) {
[155]4410 my $sql = "SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?";
[117]4411 my $result = $dbh->selectrow_hashref($sql,
[157]4412 undef, ($id) );
[186]4413 if (!$result) {
4414 $limiter++;
[244]4415##fixme: how often will this happen on a live site? fail at max limiter <n>?
[541]4416# 2013/10/22 only seems to happen when you request an entity that doesn't exist.
[186]4417 warn "no results looking for $sql with id $id (depth $limiter)\n";
4418 last;
4419 }
[157]4420 if ($result && $result->{$par_col{$type}} == $id1) {
[117]4421 $foundparent = 1;
4422 last;
[157]4423 } else {
4424##fixme: do we care about trying to return a "no such record/domain/user/group" error?
[244]4425# should be impossible to create an inconsistent DB just with API calls.
[157]4426 warn $dbh->errstr." $sql, $id" if $dbh->errstr;
[117]4427 }
4428 # group 1 is its own parent. need this here more to break strange loops than for detecting a parent
4429 last if $result->{$par_col{$type}} == 1;
[152]4430 $id = $result->{$par_col{$type}};
[117]4431 $type = $par_type{$type};
4432 }
4433
4434 return $foundparent;
4435} # end isParent()
4436
4437
[275]4438## DNSDB::zoneStatus()
4439# Returns and optionally sets a zone's status
4440# Takes a database handle, domain/revzone ID, forward/reverse flag, and optionally a status argument
4441# Returns status, or undef on errors.
4442sub zoneStatus {
[477]4443 my $self = shift;
4444 my $dbh = $self->{dbh};
[3]4445 my $id = shift;
[275]4446 my $revrec = shift;
4447 my $newstatus = shift || 'mu';
[3]4448
4449 return undef if $id !~ /^\d+$/;
4450
[283]4451 # Allow transactions, and raise an exception on errors so we can catch it later.
4452 # Use local to make sure these get "reset" properly on exiting this block
4453 local $dbh->{AutoCommit} = 0;
4454 local $dbh->{RaiseError} = 1;
4455
[275]4456 if ($newstatus ne 'mu') {
[283]4457 # ooo, fun! let's see what we were passed for status
4458 eval {
4459 $newstatus = 0 if $newstatus eq 'domoff';
4460 $newstatus = 1 if $newstatus eq 'domon';
4461 $dbh->do("UPDATE ".($revrec eq 'n' ? 'domains' : 'revzones')." SET status=? WHERE ".
[275]4462 ($revrec eq 'n' ? 'domain_id' : 'rdns_id')."=?", undef, ($newstatus,$id) );
[283]4463
4464##fixme switch to more consise "Enabled <domain"/"Disabled <domain>" as with users?
[473]4465 $resultstr = "Changed ".($revrec eq 'n' ? $self->domainName($id) : $self->revName($id)).
[283]4466 " state to ".($newstatus ? 'active' : 'inactive');
4467
4468 my %loghash;
4469 $loghash{domain_id} = $id if $revrec eq 'n';
4470 $loghash{rdns_id} = $id if $revrec eq 'y';
[473]4471 $loghash{group_id} = $self->parentID(id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec);
[283]4472 $loghash{entry} = $resultstr;
[487]4473 $self->_log(%loghash);
[283]4474
4475 $dbh->commit;
4476 };
4477 if ($@) {
4478 my $msg = $@;
4479 eval { $dbh->rollback; };
4480 $resultstr = '';
4481 $errstr = $msg;
4482 return;
4483 }
[3]4484 }
4485
[275]4486 my ($status) = $dbh->selectrow_array("SELECT status FROM ".
4487 ($revrec eq 'n' ? "domains WHERE domain_id=?" : "revzones WHERE rdns_id=?"),
4488 undef, ($id) );
[3]4489 return $status;
[275]4490} # end zoneStatus()
[3]4491
4492
[452]4493## DNSDB::getZonesByCIDR()
4494# Get a list of zone names and IDs that records for a passed CIDR block are within.
4495sub getZonesByCIDR {
[477]4496 my $self = shift;
4497 my $dbh = $self->{dbh};
[452]4498 my %args = @_;
4499
4500 my $result = $dbh->selectall_arrayref("SELECT rdns_id,revnet FROM revzones WHERE revnet >>= ? OR revnet <<= ?",
4501 { Slice => {} }, ($args{cidr}, $args{cidr}) );
4502 return $result;
4503} # end getZonesByCIDR()
4504
4505
[33]4506## DNSDB::importAXFR
4507# Import a domain via AXFR
[37]4508# Takes AXFR host, domain to transfer, group to put the domain in,
[484]4509# and an optional hash containing:
4510# status - active/inactive state flag (defaults to active)
4511# rwsoa - overwrite-SOA flag (defaults to off)
4512# rwns - overwrite-NS flag (defaults to off, doesn't affect subdomain NS records)
4513# merge - flag to automerge A or AAAA records with matching PTR records
[37]4514# Returns a status code (OK, WARN, or FAIL) and message - message should be blank
4515# if status is OK, but WARN includes conditions that are not fatal but should
4516# really be reported.
[33]4517sub importAXFR {
[484]4518 my $self = shift;
4519 my $dbh = $self->{dbh};
[35]4520 my $ifrom_in = shift;
[301]4521 my $zone = shift;
[33]4522 my $group = shift;
[484]4523
4524 my %args = @_;
4525
[33]4526##fixme: add mode to delete&replace, merge+overwrite, merge new?
4527
[484]4528 $args{status} = (defined($args{status}) ? $args{status} : 0);
4529 $args{status} = 1 if $args{status} eq 'on';
4530
[37]4531 my $nrecs = 0;
4532 my $soaflag = 0;
4533 my $nsflag = 0;
4534 my $warnmsg = '';
4535 my $ifrom;
[33]4536
[301]4537 my $rev = 'n';
[302]4538 my $code = 'OK';
4539 my $msg = 'foobar?';
[301]4540
[35]4541 # choke on possible bad setting in ifrom
[37]4542 # IPv4 and v6, and valid hostnames!
[35]4543 ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i);
4544 return ('FAIL', "Bad AXFR source host $ifrom")
4545 unless ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i);
4546
[301]4547 my $errmsg;
4548
4549 my $zone_id;
4550 my $domain_id = 0;
4551 my $rdns_id = 0;
4552 my $cidr;
4553
4554# magic happens! detect if we're importing a domain or a reverse zone
4555# while we're at it, figure out what the CIDR netblock is (if we got a .arpa)
4556# or what the formal .arpa zone is (if we got a CIDR netblock)
4557# Handles sub-octet v4 zones in the format specified in the Cricket Book, 2nd Ed, p217-218
4558
4559 if ($zone =~ m{(?:\.arpa\.?|/\d+)$}) {
4560 # we seem to have a reverse zone
4561 $rev = 'y';
4562
4563 if ($zone =~ /\.arpa\.?$/) {
4564 # we have a formal reverse zone. call _zone2cidr and get the CIDR block.
4565 ($code,$msg) = _zone2cidr($zone);
4566 return ($code, $msg) if $code eq 'FAIL';
4567 $cidr = $msg;
4568 } elsif ($zone =~ m|^[\d.]+/\d+$|) {
4569 # v4 revzone, CIDR netblock
4570 $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block");
4571 $zone = _ZONE($cidr, 'ZONE.in-addr.arpa', 'r', '.');
4572 } elsif ($zone =~ m|^[a-fA-F\d:]+/\d+$|) {
4573 # v6 revzone, CIDR netblock
4574 $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block");
4575 return ('FAIL', "$zone is not a nibble-aligned block") if $cidr->masklen % 4 != 0;
4576 $zone = _ZONE($cidr, 'ZONE.ip6.arpa', 'r', '.');
4577 } else {
4578 # there is. no. else!
4579 return ('FAIL', "Unknown zone name format");
4580 }
4581
4582 # quick check to start to see if we've already got one
4583
4584 ($zone_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?",
4585 undef, ("$cidr"));
4586 $rdns_id = $zone_id;
4587 } else {
4588 # default to domain
[349]4589 ($zone_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)",
[301]4590 undef, ($zone));
4591 $domain_id = $zone_id;
4592 }
4593
4594 return ('FAIL', ($rev eq 'n' ? 'Domain' : 'Reverse zone')." already exists") if $zone_id;
4595
[303]4596 # little local utility sub to swap $val and $host for revzone records.
4597 sub _revswap {
4598 my $rechost = shift;
4599 my $recdata = shift;
4600
4601 if ($rechost =~ /\.in-addr\.arpa\.?$/) {
4602 $rechost =~ s/\.in-addr\.arpa\.?$//;
4603 $rechost = join '.', reverse split /\./, $rechost;
4604 } else {
4605 $rechost =~ s/\.ip6\.arpa\.?$//;
4606 my @nibs = reverse split /\./, $rechost;
4607 $rechost = '';
4608 my $nc;
4609 foreach (@nibs) {
4610 $rechost.= $_;
4611 $rechost .= ":" if ++$nc % 4 == 0 && $nc < 32;
4612 }
[307]4613 $rechost .= ":" if $nc < 32 && $rechost !~ /\*$/; # close netblock records?
4614##fixme: there's a case that ends up with a partial entry here:
4615# ip:add:re:ss::
4616# can't reproduce after letting it sit overnight after discovery. :(
4617#print "$rechost\n";
[303]4618 # canonicalize with NetAddr::IP
4619 $rechost = NetAddr::IP->new($rechost)->addr unless $rechost =~ /\*$/;
4620 }
4621 return ($recdata,$rechost)
4622 }
4623
4624
[33]4625 # Allow transactions, and raise an exception on errors so we can catch it later.
4626 # Use local to make sure these get "reset" properly on exiting this block
4627 local $dbh->{AutoCommit} = 0;
4628 local $dbh->{RaiseError} = 1;
4629
[301]4630 my $sth;
4631 eval {
[34]4632
[301]4633 if ($rev eq 'n') {
[33]4634##fixme: serial
[484]4635 $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef,
4636 ($zone, $group, $args{status}) );
[301]4637 # get domain id so we can do the records
4638 ($zone_id) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')");
4639 $domain_id = $zone_id;
[487]4640 $self->_log(group_id => $group, domain_id => $domain_id,
4641 entry => "[Added ".($args{status} ? 'active' : 'inactive')." domain $zone via AXFR]");
[301]4642 } else {
4643##fixme: serial
[484]4644 $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef,
4645 ($cidr,$group,$args{status}) );
[301]4646 # get revzone id so we can do the records
4647 ($zone_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')");
4648 $rdns_id = $zone_id;
[487]4649 $self->_log(group_id => $group, rdns_id => $rdns_id,
4650 entry => "[Added ".($args{status} ? 'active' : 'inactive')." reverse zone $cidr via AXFR]");
[301]4651 }
[33]4652
[35]4653## bizarre DBI<->Net::DNS interaction bug:
4654## sometimes a zone will cause an immediate commit-and-exit (sort of) of the while()
[37]4655## fixed, apparently I was doing *something* odd, but not certain what it was that
4656## caused a commit instead of barfing
[35]4657
[34]4658 my $res = Net::DNS::Resolver->new;
[35]4659 $res->nameservers($ifrom);
[301]4660 $res->axfr_start($zone)
[35]4661 or die "Couldn't begin AXFR\n";
[34]4662
[301]4663 $sth = $dbh->prepare("INSERT INTO records (domain_id,rdns_id,host,type,val,distance,weight,port,ttl)".
4664 " VALUES (?,?,?,?,?,?,?,?,?)");
4665
[307]4666 # Stash info about sub-octet v4 revzones here so we don't have
4667 # to store the CNAMEs used to delegate a suboctet zone
4668 # $suboct{zone}{ns}[] -> array of nameservers
4669 # $suboct{zone}{cname}[] -> array of extant CNAMEs (Just In Case someone did something bizarre)
4670## commented pending actual use of this data. for now, we'll just
4671## auto-(re)create the CNAMEs in revzones on export
4672# my %suboct;
4673
[35]4674 while (my $rr = $res->axfr_next()) {
[301]4675
4676 my $val;
4677 my $distance = 0;
4678 my $weight = 0;
4679 my $port = 0;
[303]4680 my $logfrag = '';
[301]4681
[33]4682 my $type = $rr->type;
[301]4683 my $host = $rr->name;
[484]4684 my $ttl = ($args{newttl} ? $args{newttl} : $rr->ttl); # allow force-override TTLs
[35]4685
[37]4686 $soaflag = 1 if $type eq 'SOA';
4687 $nsflag = 1 if $type eq 'NS';
[35]4688
[34]4689# "Primary" types:
4690# A, NS, CNAME, SOA, PTR(warn in forward), MX, TXT, AAAA, SRV, A6(ob), SPF
4691# maybe KEY
4692
[302]4693# BIND supports:
4694# [standard]
4695# A AAAA CNAME MX NS PTR SOA TXT
4696# [variously experimental, obsolete, or obscure]
4697# 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
4698# ... if one can ever find the right magic to format them correctly
4699
4700# Net::DNS supports:
4701# RRSIG SIG NSAP NS NIMLOC NAPTR MX MR MINFO MG MB LOC ISDN IPSECKEY HINFO
4702# EID DNAME CNAME CERT APL AFSDB AAAA A DS NXT NSEC3PARAM NSEC3 NSEC KEY
4703# DNSKEY DLV X25 TXT TSIG TKEY SSHFP SRV SPF SOA RT RP PX PTR NULL APL::AplItem
4704
[35]4705# nasty big ugly case-like thing here, since we have to do *some* different
4706# processing depending on the record. le sigh.
4707
[105]4708##fixme: what record types other than TXT can/will have >255-byte payloads?
4709
[34]4710 if ($type eq 'A') {
[301]4711 $val = $rr->address;
[34]4712 } elsif ($type eq 'NS') {
[37]4713# hmm. should we warn here if subdomain NS'es are left alone?
[484]4714 next if ($args{rwns} && ($rr->name eq $zone));
[302]4715 if ($rev eq 'y') {
4716 # revzones have records more or less reversed from forward zones.
4717 my ($tmpcode,$tmpmsg) = _zone2cidr($host);
[401]4718 die "Error converting NS record: $tmpmsg\n" if $tmpcode eq 'FAIL'; # hmm. may not make sense...
[302]4719 $val = "$tmpmsg";
4720 $host = $rr->nsdname;
[303]4721 $logfrag = "Added record '$val $type $host', TTL $ttl";
[307]4722# Tag and preserve. For now this is commented for a no-op, but we have Ideas for
4723# another custom storage type ("DELEGATE") that will use these subzone-delegation records
4724#if ($val ne "$cidr") {
4725# push @{$suboct{$val}{ns}}, $host;
4726#}
[302]4727 } else {
4728 $val = $rr->nsdname;
4729 }
[35]4730 $nsflag = 1;
[34]4731 } elsif ($type eq 'CNAME') {
[302]4732 if ($rev eq 'y') {
[303]4733 # hmm. do we even want to bother with storing these at this level? Sub-octet delegation
4734 # by CNAME is essentially a record-publication hack, and we want to just represent the
4735 # "true" logical intentions as far down the stack as we can from the UI.
4736 ($host,$val) = _revswap($host,$rr->cname);
4737 $logfrag = "Added record '$val $type $host', TTL $ttl";
[307]4738# Tag and preserve in case we want to commit them as-is later, but mostly we don't care.
4739# Commented pending actually doing something with possibly new type DELEGATE
4740#my $tmprev = $host;
4741#$tmprev =~ s/^\d+\.//;
4742#($code,$tmprev) = _zone2cidr($tmprev);
4743#push @{$suboct{"$tmprev"}{cname}}, $val;
4744 # Silently skip CNAMEs in revzones.
4745 next;
[302]4746 } else {
4747 $val = $rr->cname;
4748 }
[34]4749 } elsif ($type eq 'SOA') {
[484]4750 next if $args{rwsoa};
[307]4751 $host = $rr->rname.":".$rr->mname;
[301]4752 $val = $rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum;
[35]4753 $soaflag = 1;
[34]4754 } elsif ($type eq 'PTR') {
[303]4755 ($host,$val) = _revswap($host,$rr->ptrdname);
4756 $logfrag = "Added record '$val $type $host', TTL $ttl";
[34]4757 # hmm. PTR records should not be in forward zones.
4758 } elsif ($type eq 'MX') {
[301]4759 $val = $rr->exchange;
4760 $distance = $rr->preference;
[34]4761 } elsif ($type eq 'TXT') {
4762##fixme: Net::DNS docs say this should be deprecated for rdatastr() or char_str_list(),
4763## but don't really seem enthusiastic about it.
[303]4764#print "should use rdatastr:\n\t".$rr->rdatastr."\n or char_str_list:\n\t".join(' ',$rr->char_str_list())."\n";
4765# rdatastr returns a BIND-targetted logical string, including opening and closing quotes
4766# char_str_list returns a list of the individual string fragments in the record
[307]4767# txtdata returns the more useful all-in-one form (since we want to push such protocol
4768# details as far down the stack as we can)
[303]4769# NB: this may turn out to be more troublesome if we ever have need of >512-byte TXT records.
4770 if ($rev eq 'y') {
4771 ($host,$val) = _revswap($host,$rr->txtdata);
4772 $logfrag = "Added record '$val $type $host', TTL $ttl";
4773 } else {
4774 $val = $rr->txtdata;
4775 }
[34]4776 } elsif ($type eq 'SPF') {
4777##fixme: and the same caveat here, since it is apparently a clone of ::TXT
[301]4778 $val = $rr->txtdata;
[34]4779 } elsif ($type eq 'AAAA') {
[301]4780 $val = $rr->address;
[34]4781 } elsif ($type eq 'SRV') {
[301]4782 $val = $rr->target;
4783 $distance = $rr->priority;
4784 $weight = $rr->weight;
4785 $port = $rr->port;
[34]4786 } elsif ($type eq 'KEY') {
[35]4787 # we don't actually know what to do with these...
[301]4788 $val = $rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname;
[35]4789 } else {
[301]4790 $val = $rr->rdatastr;
[35]4791 # Finding a different record type is not fatal.... just problematic.
[37]4792 # We may not be able to export it correctly.
[35]4793 $warnmsg .= "Unusual record ".$rr->name." ($type) found\n";
[33]4794 }
4795
[303]4796 my $logentry = "[AXFR ".($rev eq 'n' ? $zone : $cidr)."] ";
[34]4797
[484]4798 if ($args{merge}) {
[307]4799 if ($rev eq 'n') {
4800 # importing a domain; we have A and AAAA records that could be merged with matching PTR records
4801 my $etype;
4802 my ($erdns,$erid,$ettl) = $dbh->selectrow_array("SELECT rdns_id,record_id,ttl FROM records ".
4803 "WHERE host=? AND val=? AND type=12",
4804 undef, ($host, $val) );
4805 if ($erid) {
4806 if ($type eq 'A') { # PTR -> A+PTR
4807 $etype = 65280;
4808 $logentry .= "Merged A record with existing PTR record '$host A+PTR $val', TTL $ettl";
4809 }
4810 if ($type eq 'AAAA') { # PTR -> AAAA+PTR
4811 $etype = 65281;
4812 $logentry .= "Merged AAAA record with existing PTR record '$host AAAA+PTR $val', TTL $ettl";
4813 }
4814 $ettl = ($ettl < $ttl ? $ettl : $ttl); # use lower TTL
4815 $dbh->do("UPDATE records SET domain_id=?,ttl=?,type=? WHERE record_id=?", undef,
4816 ($domain_id, $ettl, $etype, $erid));
4817 $nrecs++;
[487]4818 $self->_log(group_id => $group, domain_id => $domain_id, rdns_id => $erdns, entry => $logentry);
[307]4819 next; # while axfr_next
4820 }
4821 } # $rev eq 'n'
4822 else {
4823 # importing a revzone, we have PTR records that could be merged with matching A/AAAA records
4824 my ($domid,$erid,$ettl,$etype) = $dbh->selectrow_array("SELECT domain_id,record_id,ttl,type FROM records ".
4825 "WHERE host=? AND val=? AND (type=1 OR type=28)",
4826 undef, ($host, $val) );
4827 if ($erid) {
4828 if ($etype == 1) { # A -> A+PTR
4829 $etype = 65280;
4830 $logentry .= "Merged PTR record with existing matching A record '$host A+PTR $val', TTL $ettl";
4831 }
4832 if ($etype == 28) { # AAAA -> AAAA+PTR
4833 $etype = 65281;
4834 $logentry .= "Merged PTR record with existing matching AAAA record '$host AAAA+PTR $val', TTL $ettl";
4835 }
4836 $ettl = ($ettl < $ttl ? $ettl : $ttl); # use lower TTL
4837 $dbh->do("UPDATE records SET rdns_id=?,ttl=?,type=? WHERE record_id=?", undef,
4838 ($rdns_id, $ettl, $etype, $erid));
4839 $nrecs++;
[487]4840 $self->_log(group_id => $group, domain_id => $domid, rdns_id => $rdns_id, entry => $logentry);
[307]4841 next; # while axfr_next
4842 }
4843 } # $rev eq 'y'
[484]4844 } # if $args{merge}
[34]4845
[302]4846 # Insert the new record
[301]4847 $sth->execute($domain_id, $rdns_id, $host, $reverse_typemap{$type}, $val,
4848 $distance, $weight, $port, $ttl);
4849
[37]4850 $nrecs++;
[34]4851
[301]4852 if ($type eq 'SOA') {
[484]4853 # also !$args{rwsoa}, but if that's set, it should be impossible to get here.
[301]4854 my @tmp1 = split /:/, $host;
4855 my @tmp2 = split /:/, $val;
4856 $logentry .= "Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ".
4857 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl";
[303]4858 } elsif ($logfrag) {
4859 # special case for log entries we need to meddle with a little.
4860 $logentry .= $logfrag;
[301]4861 } else {
4862 $logentry .= "Added record '$host $type";
4863 $logentry .= " [distance $distance]" if $type eq 'MX';
4864 $logentry .= " [priority $distance] [weight $weight] [port $port]" if $type eq 'SRV';
4865 $logentry .= " $val', TTL $ttl";
4866 }
[487]4867 $self->_log(group_id => $group, domain_id => $domain_id, rdns_id => $rdns_id, entry => $logentry);
[301]4868
[37]4869 } # while axfr_next
4870
[307]4871# Detect and handle delegated subzones
4872# Placeholder for when we decide what to actually do with this, see previous comments in NS and CNAME handling.
4873#foreach (keys %suboct) {
4874# print "found ".($suboct{$_}{ns} ? @{$suboct{$_}{ns}} : '0')." NS records and ".
4875# ($suboct{$_}{cname} ? @{$suboct{$_}{cname}} : '0')." CNAMEs for $_\n";
4876#}
4877
[37]4878 # Overwrite SOA record
[484]4879 if ($args{rwsoa}) {
[37]4880 $soaflag = 1;
4881 my $sthgetsoa = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
4882 my $sthputsoa = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)");
4883 $sthgetsoa->execute($group,$reverse_typemap{SOA});
4884 while (my ($host,$val,$ttl) = $sthgetsoa->fetchrow_array()) {
[301]4885 $host =~ s/DOMAIN/$zone/g;
4886 $val =~ s/DOMAIN/$zone/g;
4887 $sthputsoa->execute($zone_id,$host,$reverse_typemap{SOA},$val,$ttl);
[34]4888 }
[37]4889 }
[34]4890
[37]4891 # Overwrite NS records
[484]4892 if ($args{rwns}) {
[37]4893 $nsflag = 1;
4894 my $sthgetns = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
4895 my $sthputns = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)");
4896 $sthgetns->execute($group,$reverse_typemap{NS});
4897 while (my ($host,$val,$ttl) = $sthgetns->fetchrow_array()) {
[301]4898 $host =~ s/DOMAIN/$zone/g;
4899 $val =~ s/DOMAIN/$zone/g;
4900 $sthputns->execute($zone_id,$host,$reverse_typemap{NS},$val,$ttl);
[37]4901 }
4902 }
[34]4903
[35]4904 die "No records found; either $ifrom is not authoritative or doesn't allow transfers\n" if !$nrecs;
4905 die "Bad zone: No SOA record!\n" if !$soaflag;
4906 die "Bad zone: No NS records!\n" if !$nsflag;
4907
[37]4908 $dbh->commit;
[35]4909
[33]4910 };
4911
4912 if ($@) {
4913 my $msg = $@;
4914 eval { $dbh->rollback; };
[34]4915 return ('FAIL',$msg." $warnmsg");
[33]4916 } else {
[35]4917 return ('WARN', $warnmsg) if $warnmsg;
[91]4918 return ('OK',"Imported OK");
[33]4919 }
4920
[37]4921 # it should be impossible to get here.
[34]4922 return ('WARN',"OOOK!");
[33]4923} # end importAXFR()
4924
4925
[302]4926## DNSDB::importBIND()
4927sub importBIND {
4928} # end importBIND()
4929
4930
4931## DNSDB::import_tinydns()
4932sub import_tinydns {
4933} # end import_tinydns()
4934
4935
[103]4936## DNSDB::export()
4937# Export the DNS database, or a part of it
[485]4938# Takes a string indicating the export type, plus optional arguments depending on type
[103]4939# Writes zone data to targets as appropriate for type
4940sub export {
[485]4941 my $self = shift;
[103]4942 my $target = shift;
4943
4944 if ($target eq 'tiny') {
[485]4945 $self->__export_tiny(@_);
[103]4946 }
4947# elsif ($target eq 'foo') {
[485]4948# __export_foo(@_);
[103]4949#}
4950# etc
4951
4952} # end export()
4953
4954
4955## DNSDB::__export_tiny
4956# Internal sub to implement tinyDNS (compatible) export
[485]4957# Takes filehandle to write export to, optional argument(s)
[103]4958# to determine which data gets exported
4959sub __export_tiny {
[485]4960 my $self = shift;
4961 my $dbh = $self->{dbh};
[103]4962 my $datafile = shift;
[559]4963 my $zonefilehandle = $datafile; # makes cache/no-cache a little simpler
[103]4964
4965##fixme: slurp up further options to specify particular zone(s) to export
4966
4967##fixme: fail if $datafile isn't an open, writable file
4968
4969 # easy case - export all evarything
4970 # not-so-easy case - export item(s) specified
4971 # todo: figure out what kind of list we use to export items
4972
[329]4973# raw packet in unknown format: first byte indicates length
4974# of remaining data, allows up to 255 raw bytes
4975
[372]4976 # Locations/views - worth including in the caching setup?
4977 my $lochash = $dbh->selectall_hashref("SELECT location,iplist FROM locations", 'location');
4978 foreach my $location (keys %$lochash) {
[375]4979 foreach my $ipprefix (split /[,\s]+/, $lochash->{$location}{iplist}) {
4980 $ipprefix =~ s/\s+//g;
[449]4981 $ipprefix = new NetAddr::IP $ipprefix;
4982##fixme: how to handle IPv6?
4983next if $ipprefix->{isv6};
4984 # have to account for /nn CIDR entries. tinydns only speaks octet-sliced prefix.
4985 if ($ipprefix->masklen <= 8) {
4986 foreach ($ipprefix->split(8)) {
4987 my $tmp = $_->addr;
4988 $tmp =~ s/\.\d+\.\d+\.\d+$//;
4989 print $datafile "%$location:$tmp\n";
4990 }
4991 } elsif ($ipprefix->masklen <= 16) {
4992 foreach ($ipprefix->split(16)) {
4993 my $tmp = $_->addr;
4994 $tmp =~ s/\.\d+\.\d+$//;
4995 print $datafile "%$location:$tmp\n";
4996 }
4997 } elsif ($ipprefix->masklen <= 24) {
4998 foreach ($ipprefix->split(24)) {
4999 my $tmp = $_->addr;
5000 $tmp =~ s/\.\d+$//;
5001 print $datafile "%$location:$tmp\n";
5002 }
5003 } else {
5004 foreach ($ipprefix->split(32)) {
5005 print $datafile "%$location:".$_->addr."\n";
5006 }
5007 }
[372]5008 }
5009 print $datafile "%$location\n" if !$lochash->{$location}{iplist};
5010 }
5011
[329]5012 # tracking hash so we don't double-export A+PTR or AAAA+PTR records.
5013 my %recflags;
5014
[523]5015# For reasons unknown, we can't sanely UNION these statements. Feh.
5016# Supposedly it should work though (note last 3 lines):
5017## PG manual
5018#UNION Clause
5019#
5020#The UNION clause has this general form:
5021#
5022# select_statement UNION [ ALL ] select_statement
5023#
5024#select_statement is any SELECT statement without an ORDER BY, LIMIT, FOR UPDATE, or FOR SHARE clause. (ORDER BY
5025#and LIMIT can be attached to a subexpression if it is enclosed in parentheses. Without parentheses, these
5026#clauses will be taken to apply to the result of the UNION, not to its right-hand input expression.)
5027 my $soasth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
5028 "FROM records WHERE rdns_id=? AND type=6");
[543]5029 my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
[523]5030 "FROM records WHERE rdns_id=? AND not type=6 ".
5031 "ORDER BY masklen(CAST(val AS inet)) DESC, CAST(val AS inet)");
5032 my $revsth = $dbh->prepare("SELECT rdns_id,revnet,status,changed FROM revzones WHERE status=1 ".
5033 "ORDER BY masklen(revnet) DESC");
5034 my $zonesth = $dbh->prepare("UPDATE revzones SET changed='n' WHERE rdns_id=?");
5035 $revsth->execute();
5036 while (my ($revid,$revzone,$revstat,$changed) = $revsth->fetchrow_array) {
[368]5037##fixme: need to find a way to block opening symlinked files without introducing a race.
5038# O_NOFOLLOW
5039# If pathname is a symbolic link, then the open fails. This is a FreeBSD extension, which was
5040# added to Linux in version 2.1.126. Symbolic links in earlier components of the pathname will
5041# still be followed.
5042# but that doesn't help other platforms. :/
[523]5043 my $tmpzone = NetAddr::IP->new($revzone);
5044##fixme: locations/views? subnet mask? need to avoid possible collisions with zone/superzone
5045## (eg /20 vs /24, starting on .0.0)
5046 my $cz = $tmpzone->network->addr."-".$tmpzone->masklen;
5047 my $cachefile = "$self->{exportcache}/$cz";
5048 my $tmpcache = "$self->{exportcache}/tmp.$cz.$$";
[521]5049 eval {
[108]5050
[559]5051 # write fresh records if:
5052 # - we are not using the cache
5053 # - force_refresh is set
5054 # - the zone has changed
5055 # - the cache file does not exist
5056 # - the cache file is empty
5057 if (!$self->{usecache} || $self->{force_refresh} || $changed || !-e $cachefile || -z $cachefile) {
5058 if ($self->{usecache}) {
5059 open ZONECACHE, ">$tmpcache" or die "Error creating temporary file $tmpcache: $!\n";
5060 $zonefilehandle = *ZONECACHE;
5061 }
[521]5062
[523]5063 # need to fetch this separately since the rest of the records all (should) have real IPs in val
5064 $soasth->execute($revid);
5065 my (@zsoa) = $soasth->fetchrow_array();
[559]5066 _printrec_tiny($zonefilehandle,'y',\%recflags,$revzone,
[523]5067 $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
5068
5069 $recsth->execute($revid);
[543]5070 while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
[523]5071 next if $recflags{$recid};
[521]5072
[543]5073# not sure this is necessary for revzones.
5074# # Spaces are evil.
5075# $val =~ s/^\s+//;
5076# $val =~ s/\s+$//;
5077# if ($typemap{$type} ne 'TXT') {
5078# # Leading or trailng spaces could be legit in TXT records.
5079# $host =~ s/^\s+//;
5080# $host =~ s/\s+$//;
5081# }
[372]5082
[559]5083 _printrec_tiny($zonefilehandle, 'y', \%recflags, $revzone,
5084 $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive);
[450]5085
[523]5086 $recflags{$recid} = 1;
[368]5087
[521]5088 } # while ($recsth)
5089
[559]5090 if ($self->{usecache}) {
5091 close ZONECACHE; # force the file to be written
5092 # catch obvious write errors that leave an empty temp file
5093 if (-s $tmpcache) {
5094 rename $tmpcache, $cachefile
5095 or die "Error overwriting cache file $cachefile with temporary file: $!\n";
5096 }
[521]5097 }
5098
5099 } # if $changed or cache filesize is 0
5100
5101 };
5102 if ($@) {
[523]5103 print "error writing new data for $revzone: $@\n";
[521]5104 # error! something borked, and we should be able to fall back on the old cache file
5105 # report the error, somehow.
5106 } else {
[523]5107 # mark zone as unmodified. Only do this if no errors, that way
[521]5108 # export failures should recover a little more automatically.
[523]5109 $zonesth->execute($revid);
[368]5110 }
[521]5111
[559]5112 if ($self->{usecache}) {
5113 # We've already made as sure as we can that a cached zone file is "good",
5114 # although possibly stale/obsolete due to errors creating a new one.
5115 open CACHE, "<$cachefile";
5116 print $datafile $_ while <CACHE>;
5117 close CACHE;
5118 }
5119
[523]5120 } # while ($revsth)
[329]5121
[523]5122 my $domsth = $dbh->prepare("SELECT domain_id,domain,status,changed FROM domains WHERE status=1");
[543]5123 $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
[523]5124 "FROM records WHERE domain_id=?"); # Just exclude all types relating to rDNS
5125# "FROM records WHERE domain_id=? AND type < 65280"); # Just exclude all types relating to rDNS
5126 $zonesth = $dbh->prepare("UPDATE domains SET changed='n' WHERE domain_id=?");
5127 $domsth->execute();
5128 while (my ($domid,$dom,$domstat,$changed) = $domsth->fetchrow_array) {
[368]5129##fixme: need to find a way to block opening symlinked files without introducing a race.
5130# O_NOFOLLOW
5131# If pathname is a symbolic link, then the open fails. This is a FreeBSD extension, which was
5132# added to Linux in version 2.1.126. Symbolic links in earlier components of the pathname will
5133# still be followed.
5134# but that doesn't help other platforms. :/
[523]5135 my $cachefile = "$self->{exportcache}/$dom";
5136 my $tmpcache = "$self->{exportcache}/tmp.$dom.$$";
[521]5137 eval {
[344]5138
[559]5139 # write fresh records if:
5140 # - we are not using the cache
5141 # - force_refresh is set
5142 # - the zone has changed
5143 # - the cache file does not exist
5144 # - the cache file is empty
5145 if (!$self->{usecache} || $self->{force_refresh} || $changed || !-e $cachefile || -z $cachefile) {
5146 if ($self->{usecache}) {
5147 open ZONECACHE, ">$tmpcache" or die "Error creating temporary file $tmpcache: $!\n";
5148 $zonefilehandle = *ZONECACHE;
5149 }
[329]5150
[523]5151 $recsth->execute($domid);
[543]5152 while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
[523]5153 next if $recflags{$recid};
[521]5154
[523]5155 # Spaces are evil.
5156 $host =~ s/^\s+//;
5157 $host =~ s/\s+$//;
5158 if ($typemap{$type} ne 'TXT') {
5159 # Leading or trailng spaces could be legit in TXT records.
5160 $val =~ s/^\s+//;
5161 $val =~ s/\s+$//;
5162 }
5163
[559]5164 _printrec_tiny($zonefilehandle, 'n', \%recflags,
5165 $dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive);
[329]5166
[523]5167 $recflags{$recid} = 1;
[450]5168
[521]5169 } # while ($recsth)
5170
5171
[559]5172 if ($self->{usecache}) {
5173 close ZONECACHE; # force the file to be written
5174 # catch obvious write errors that leave an empty temp file
5175 if (-s $tmpcache) {
5176 rename $tmpcache, $cachefile
5177 or die "Error overwriting cache file $cachefile with temporary file: $!\n";
5178 }
[521]5179 }
5180
5181 } # if $changed or cache filesize is 0
5182
5183 };
5184 if ($@) {
[523]5185 print "error writing new data for $dom: $@\n";
[521]5186 # error! something borked, and we should be able to fall back on the old cache file
5187 # report the error, somehow.
5188 } else {
[523]5189 # mark domain as unmodified. Only do this if no errors, that way
[521]5190 # export failures should recover a little more automatically.
[523]5191 $zonesth->execute($domid);
[368]5192 }
[521]5193
[559]5194 if ($self->{usecache}) {
5195 # We've already made as sure as we can that a cached zone file is "good",
5196 # although possibly stale/obsolete due to errors creating a new one.
5197 open CACHE, "<$cachefile";
5198 print $datafile $_ while <CACHE>;
5199 close CACHE;
5200 }
5201
[329]5202 } # while ($domsth)
5203
5204} # end __export_tiny()
5205
5206
5207# Utility sub for __export_tiny above
5208sub _printrec_tiny {
[543]5209 my ($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp,$expires,$stampactive) = @_;
[329]5210
[543]5211 $loc = '' if !$loc; # de-nullify - just in case
5212##fixme: handle case of record-with-location-that-doesn't-exist better.
5213# note this currently fails safe (tested) - records with a location that
5214# doesn't exist will not be sent to any client
5215# $loc = '' if !$lochash->{$loc};
5216
5217
5218## Records that are valid only before or after a set time
5219
5220# record due to expire sometime is the complex case. we don't want to just
5221# rely on tinydns' auto-adjusting TTLs, because the default TTL in that case
5222# is one day instead of the SOA minttl as BIND might do.
5223
5224# consider the case where a record is set to expire a week ahead, but the next
5225# day later you want to change it NOW (or as NOWish as you get with your DNS
5226# management practice). but now you're stuck, because someone, somewhere,
5227# has just done a lookup before your latest change was published, and they'll
5228# be caching that old, broken record for 1 day instead of your zone default
5229# TTL.
5230
5231# $stamp-$ttl is the *latest* we can publish the record with the defined TTL
5232# to still have the expiry happen as scheduled, but we need to find some
5233# *earlier* point. We can maybe guess, and 2x TTL is probably reasonable,
5234# but we need info on the export frequency.
5235
5236# export the normal, non-expiring record up until $stamp-<guesstimate>, then
5237# switch to exporting a record with the TAI64 stamp and a 0 TTL so tinydns
5238# takes over TTL management.
5239
5240 if ($stampactive) {
5241 if ($expires) {
5242 # record expires at $stamp; decide if we need to keep the TTL and ignore
5243 # the stamp for a time or if we need to change the TTL to 0 and convert
5244 # $stamp to TAI64 so tinydns can use $stamp to autoadjust the TTL on the fly.
5245# extra hack, optimally needs more knowledge of data export frequency
5246# smack the idiot customer who insists on 0 TTLs; they can suck up and
5247# deal with a 10-minute TTL. especially on scheduled changes. note this
5248# should be (export freq * 2), but we don't know the actual export frequency.
5249$ttl = 300 if $ttl == 0; #hack phtui
5250 my $ahead = (86400 < $ttl*2 ? 86400 : $ttl*2);
5251 if ((time() + $ahead) < $stamp) {
5252 # more than 2x TTL OR more than one day (whichever is less) from expiry time; publish normal record
5253 $stamp = '';
5254 } else {
5255 # less than 2x TTL from expiry time, let tinydns take over TTL management and publish the TAI64 stamp.
5256 $ttl = 0;
5257 $stamp = unixtai64($stamp);
5258 $stamp =~ s/\@//;
5259 }
5260 } else {
5261 # record is "active after"; convert epoch from database to TAI64, publish, and collect $200.
5262 $stamp = unixtai64($stamp);
5263 $stamp =~ s/\@//;
5264 }
5265 } else {
5266 # flag for active timestamp is false; don't actually put a timestamp in the output
5267 $stamp = '';
5268 }
5269
5270 # support tinydns' auto-TTL
5271 $ttl = '' if $ttl == -1;
5272# these are WAY FREAKING HIGH - higher even than most TLD registry TTLs!
5273# NS 259200 => 3d
5274# all others 86400 => 1d
5275
[531]5276 if ($revrec eq 'y') {
5277 $val = $zone if $val eq '@';
5278 } else {
5279 $host = $zone if $host eq '@';
5280 }
5281
[329]5282 ## Convert a bare number into an octal-coded pair of octets.
5283 # Take optional arg to indicate a decimal or hex input. Defaults to hex.
5284 sub octalize {
5285 my $tmp = shift;
5286 my $srctype = shift || 'h'; # default assumes hex string
5287 $tmp = sprintf "%0.4x", hex($tmp) if $srctype eq 'h'; # 0-pad hex to 4 digits
5288 $tmp = sprintf "%0.4x", $tmp if $srctype eq 'd'; # 0-pad decimal to 4 hex digits
5289 my @o = ($tmp =~ /^(..)(..)$/); # split into octets
5290 return sprintf "\\%0.3o\\%0.3o", hex($o[0]), hex($o[1]);;
5291 }
5292
[339]5293## WARNING: This works to export even the whole Internet's worth of IP space...
5294## if you have the disk/RAM to handle the dataset, and you call this sub based on /16-sized chunks
5295## A /16 took ~3 seconds with a handful of separate records; adding a /8 pushed export time out to ~13m:40s
5296## 0/0 is estimated to take ~54 hours and ~256G of disk
5297## RAM usage depends on how many non-template entries you have in the set.
5298## This should probably be done on record addition rather than export; large blocks may need to be done in a
5299## forked process
5300 sub __publish_subnet {
5301 my $sub = shift;
5302 my $recflags = shift;
5303 my $hpat = shift;
5304 my $fh = shift;
5305 my $ttl = shift;
5306 my $stamp = shift;
5307 my $loc = shift;
5308 my $ptronly = shift || 0;
5309
5310 my $iplist = $sub->splitref(32);
5311 foreach (@$iplist) {
5312 my $ip = $_->addr;
5313 # make as if we split the non-octet-aligned block into octet-aligned blocks as with SOA
5314 next if $ip =~ /\.(0|255)$/;
5315 next if $$recflags{$ip};
5316 $$recflags{$ip}++;
[463]5317 next if $hpat eq '%blank%'; # Allows blanking a subnet so no records are published.
[339]5318 my $rec = $hpat; # start fresh with the template for each IP
5319 _template4_expand(\$rec, $ip);
5320 print $fh ($ptronly ? "^"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$rec" : "=$rec:$ip").
5321 ":$ttl:$stamp:$loc\n";
5322 }
5323 }
5324
[103]5325##fixme? append . to all host/val hostnames
5326 if ($typemap{$type} eq 'SOA') {
5327
5328 # host contains pri-ns:responsible
5329 # val is abused to contain refresh:retry:expire:minttl
5330##fixme: "manual" serial vs tinydns-autoserial
[202]5331 # let's be explicit about abusing $host and $val
5332 my ($email, $primary) = (split /:/, $host)[0,1];
5333 my ($refresh, $retry, $expire, $min_ttl) = (split /:/, $val)[0,1,2,3];
[330]5334 if ($revrec eq 'y') {
[337]5335##fixme: have to publish SOA records for each v4 /24 in sub-/16, and each /16 in sub-/8
5336# what about v6?
5337# -> only need SOA for local chunks offset from reverse delegation boundaries, so v6 is fine
[330]5338 $zone = NetAddr::IP->new($zone);
[337]5339 # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones
5340 if (!$zone->{isv6} && ($zone->masklen < 24) && ($zone->masklen % 8 != 0)) {
5341 foreach my $szone ($zone->split($zone->masklen + (8 - $zone->masklen % 8))) {
5342 $szone = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.');
5343 print $datafile "Z$szone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n";
5344 }
5345 return; # skips "default" bits just below
5346 }
[330]5347 $zone = _ZONE($zone, 'ZONE', 'r', '.').($zone->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
5348 }
[329]5349 print $datafile "Z$zone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n";
[103]5350
5351 } elsif ($typemap{$type} eq 'A') {
5352
[108]5353 print $datafile "+$host:$val:$ttl:$stamp:$loc\n";
[103]5354
5355 } elsif ($typemap{$type} eq 'NS') {
5356
[330]5357 if ($revrec eq 'y') {
5358 $val = NetAddr::IP->new($val);
[345]5359 # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones
5360 if (!$val->{isv6} && ($val->masklen < 24) && ($val->masklen % 8 != 0)) {
[346]5361 foreach my $szone ($val->split($val->masklen + (8 - $val->masklen % 8))) {
[345]5362 my $szone2 = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.');
[346]5363 next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen;
[345]5364 print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n";
[346]5365 $$recflags{$szone2} = $val->masklen;
[345]5366 }
[346]5367 } elsif ($val->{isv6} && ($val->masklen < 64) && ($val->masklen % 4 !=0)) {
5368 foreach my $szone ($val->split($val->masklen + (4 - $val->masklen % 4))) {
5369 my $szone2 = _ZONE($szone, 'ZONE.ip6.arpa', 'r', '.');
5370 next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen;
5371 print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n";
5372 $$recflags{$szone2} = $val->masklen;
5373 }
5374 } else {
5375 my $val2 = _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
5376 print $datafile "\&$val2"."::$host:$ttl:$stamp:$loc\n";
5377 $$recflags{$val2} = $val->masklen;
[345]5378 }
[330]5379 } else {
5380 print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n";
5381 }
[103]5382
5383 } elsif ($typemap{$type} eq 'AAAA') {
5384
5385 print $datafile ":$host:28:";
5386 my $altgrp = 0;
5387 my @altconv;
[108]5388 # Split in to up to 8 groups of hex digits (allows for IPv6 :: 0-collapsing)
[103]5389 foreach (split /:/, $val) {
5390 if (/^$/) {
5391 # flag blank entry; this is a series of 0's of (currently) unknown length
5392 $altconv[$altgrp++] = 's';
5393 } else {
5394 # call sub to convert 1-4 hex digits to 2 string-rep octal bytes
5395 $altconv[$altgrp++] = octalize($_)
5396 }
5397 }
5398 foreach my $octet (@altconv) {
5399 # if not 's', output
5400 print $datafile $octet unless $octet =~ /^s$/;
5401 # if 's', output (9-array length)x literal '\000\000'
5402 print $datafile '\000\000'x(9-$altgrp) if $octet =~ /^s$/;
5403 }
[108]5404 print $datafile ":$ttl:$stamp:$loc\n";
[103]5405
5406 } elsif ($typemap{$type} eq 'MX') {
5407
[108]5408 print $datafile "\@$host"."::$val:$dist:$ttl:$stamp:$loc\n";
[103]5409
5410 } elsif ($typemap{$type} eq 'TXT') {
5411
5412##fixme: split v-e-r-y long TXT strings? will need to do so for BIND export, at least
[350]5413 if ($revrec eq 'n') {
5414 $val =~ s/:/\\072/g; # may need to replace other symbols
5415 print $datafile "'$host:$val:$ttl:$stamp:$loc\n";
5416 } else {
5417 $host =~ s/:/\\072/g; # may need to replace other symbols
5418 my $val2 = NetAddr::IP->new($val);
5419 print $datafile "'"._ZONE($val2, 'ZONE', 'r', '.').($val2->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
5420 ":$host:$ttl:$stamp:$loc\n";
5421 }
[103]5422
5423# by-hand TXT
5424#:deepnet.cx:16:2v\075spf1\040a\040a\072bacon.deepnet.cx\040a\072home.deepnet.cx\040-all:3600
5425#@ IN TXT "v=spf1 a a:bacon.deepnet.cx a:home.deepnet.cx -all"
5426#'deepnet.cx:v=spf1 a a\072bacon.deepnet.cx a\072home.deepnet.cx -all:3600
5427
5428#txttest IN TXT "v=foo bar:bob kn;ob' \" !@#$%^&*()-=_+[]{}<>?"
5429#: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
5430
5431# very long TXT record as brought in by axfr-get
5432# note tinydns does not support >512-byte RR data, need axfr-dns (for TCP support) for that
5433# also note, tinydns does not seem to support <512, >256-byte RRdata from axfr-get either. :/
5434#:longtxt.deepnet.cx:16:
5435#\170this is a very long txt record. it is really long. long. very long. really very long. this is a very long txt record.
5436#\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.
5437#\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.
5438#:3600
5439
5440 } elsif ($typemap{$type} eq 'CNAME') {
5441
[372]5442 if ($revrec eq 'n') {
5443 print $datafile "C$host:$val:$ttl:$stamp:$loc\n";
5444 } else {
5445 my $val2 = NetAddr::IP->new($val);
5446 print $datafile "C"._ZONE($val2, 'ZONE', 'r', '.').($val2->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
5447 ":$host:$ttl:$stamp:$loc\n";
5448 }
[103]5449
5450 } elsif ($typemap{$type} eq 'SRV') {
5451
5452 # data is two-byte values for priority, weight, port, in that order,
5453 # followed by length/string data
5454
5455 print $datafile ":$host:33:".octalize($dist,'d').octalize($weight,'d').octalize($port,'d');
5456
5457 $val .= '.' if $val !~ /\.$/;
5458 foreach (split /\./, $val) {
5459 printf $datafile "\\%0.3o%s", length($_), $_;
5460 }
[108]5461 print $datafile "\\000:$ttl:$stamp:$loc\n";
[103]5462
5463 } elsif ($typemap{$type} eq 'RP') {
5464
5465 # RP consists of two mostly free-form strings.
5466 # The first is supposed to be an email address with @ replaced by . (as with the SOA contact)
5467 # The second is the "hostname" of a TXT record with more info.
5468 print $datafile ":$host:17:";
5469 my ($who,$what) = split /\s/, $val;
5470 foreach (split /\./, $who) {
5471 printf $datafile "\\%0.3o%s", length($_), $_;
5472 }
5473 print $datafile '\000';
5474 foreach (split /\./, $what) {
5475 printf $datafile "\\%0.3o%s", length($_), $_;
5476 }
[108]5477 print $datafile "\\000:$ttl:$stamp:$loc\n";
[103]5478
5479 } elsif ($typemap{$type} eq 'PTR') {
5480
[332]5481 $zone = NetAddr::IP->new($zone);
[337]5482 $$recflags{$val}++;
[332]5483 if (!$zone->{isv6} && $zone->masklen > 24) {
5484 ($val) = ($val =~ /\.(\d+)$/);
5485 print $datafile "^$val."._ZONE($zone, 'ZONE', 'r', '.').'.in-addr.arpa'.
5486 ":$host:ttl:$stamp:$loc\n";
5487 } else {
5488 $val = NetAddr::IP->new($val);
5489 print $datafile "^".
5490 _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
[334]5491 ":$host:$ttl:$stamp:$loc\n";
[332]5492 }
[103]5493
[333]5494 } elsif ($type == 65280) { # A+PTR
[334]5495
[337]5496 $$recflags{$val}++;
[334]5497 print $datafile "=$host:$val:$ttl:$stamp:$loc\n";
5498
[333]5499 } elsif ($type == 65281) { # AAAA+PTR
5500
[526]5501 $$recflags{$val}++;
[333]5502 # treat these as two separate records. since tinydns doesn't have
5503 # a native combined type, we have to create them separately anyway.
[526]5504 # print both; a dangling record is harmless, and impossible via web
5505 # UI anyway
5506 _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,28,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
5507 _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,12,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
[335]5508##fixme: add a config flag to indicate use of the patch from http://www.fefe.de/dns/
5509# type 6 is for AAAA+PTR, type 3 is for AAAA
[333]5510
5511 } elsif ($type == 65282) { # PTR template
[335]5512
5513 # only useful for v4 with standard DNS software, since this expands all
5514 # IPs in $zone (or possibly $val?) with autogenerated records
[337]5515 $val = NetAddr::IP->new($val);
5516 return if $val->{isv6};
[336]5517
[339]5518 if ($val->masklen <= 16) {
5519 foreach my $sub ($val->split(16)) {
5520 __publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, 1);
5521 }
5522 } else {
5523 __publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, 1);
[337]5524 }
[335]5525
[333]5526 } elsif ($type == 65283) { # A+PTR template
[335]5527
[337]5528 $val = NetAddr::IP->new($val);
5529 # Just In Case. An A+PTR should be impossible to add to a v6 revzone via API.
5530 return if $val->{isv6};
[339]5531
5532 if ($val->masklen <= 16) {
5533 foreach my $sub ($val->split(16)) {
5534 __publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, 0);
5535 }
5536 } else {
5537 __publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, 0);
[337]5538 }
[335]5539
[333]5540 } elsif ($type == 65284) { # AAAA+PTR template
[335]5541 # Stub for completeness. Could be exported to DNS software that supports
5542 # some degree of internal automagic in generic-record-creation
5543 # (eg http://search.cpan.org/dist/AllKnowingDNS/ )
[333]5544
[345]5545 } elsif ($type == 65285) { # Delegation
5546 # This is intended for reverse zones, but may prove useful in forward zones.
5547
[346]5548 # All delegations need to create one or more NS records. The NS record handler knows what to do.
5549 _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,$reverse_typemap{'NS'},
5550 $val,$dist,$weight,$port,$ttl,$loc,$stamp);
5551 if ($revrec eq 'y') {
5552 # In the case of a sub-/24 v4 reverse delegation, we need to generate CNAMEs
5553 # to redirect all of the individual IP lookups as well.
5554 # Not sure how this would actually resolve if a /24 or larger was delegated
5555 # one way, and a sub-/24 in that >=/24 was delegated elsewhere...
5556 my $dblock = NetAddr::IP->new($val);
5557 if (!$dblock->{isv6} && $dblock->masklen > 24) {
5558 my @subs = $dblock->split;
5559 foreach (@subs) {
5560 next if $$recflags{"$_"};
5561 my ($oct) = ($_->addr =~ /(\d+)$/);
5562 print $datafile "C"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$oct.".
5563 _ZONE($dblock, 'ZONE.in-addr.arpa', 'r', '.').":$ttl:$stamp:$loc\n";
5564 $$recflags{"$_"}++;
5565 }
5566 }
5567 }
[345]5568
[364]5569##
5570## Uncommon types. These will need better UI support Any Day Sometime Maybe(TM).
5571##
5572
5573 } elsif ($type == 44) { # SSHFP
5574 my ($algo,$fpt,$fp) = split /\s+/, $val;
5575
5576 my $rec = sprintf ":$host:44:\\%0.3o\\%0.3o", $algo, $fpt;
5577 while (my ($byte) = ($fp =~ /^(..)/) ) {
5578 $rec .= sprintf "\\%0.3o", hex($byte);
5579 $fp =~ s/^..//;
5580 }
5581 print $datafile "$rec:$ttl:$stamp:$loc\n";
5582
[345]5583 } else {
[108]5584 # raw record. we don't know what's in here, so we ASS-U-ME the user has
5585 # put it in correctly, since either the user is messing directly with the
5586 # database, or the record was imported via AXFR
5587 # <split by char>
5588 # convert anything not a-zA-Z0-9.- to octal coding
5589
5590##fixme: add flag to export "unknown" record types - note we'll probably end up
5591# mangling them since they were written to the DB from Net::DNS::RR::<type>->rdatastr.
5592 #print $datafile ":$host:$type:$val:$ttl:$stamp:$loc\n";
5593
[103]5594 } # record type if-else
5595
[329]5596} # end _printrec_tiny()
[103]5597
5598
[197]5599## DNSDB::mailNotify()
[283]5600# Sends notification mail to recipients regarding a DNSDB operation
[197]5601sub mailNotify {
[483]5602 my $self = shift;
5603 my $dbh = $self->{dbh};
[197]5604 my ($subj,$message) = @_;
5605
[517]5606 return if $self->{mailhost} eq 'smtp.example.com'; # do nothing if still using default SMTP host.
[197]5607
[517]5608 my $mailer = Net::SMTP->new($self->{mailhost}, Hello => "dnsadmin.$self->{domain}");
[197]5609
[517]5610 my $mailsender = ($self->{mailsender} ? $self->{mailsender} : $self->{mailnotify});
[197]5611
5612 $mailer->mail($mailsender);
[517]5613 $mailer->to($self->{mailnotify});
5614 $mailer->data("From: \"$self->{mailname}\" <$mailsender>\n",
5615 "To: <$self->{mailnotify}>\n",
[197]5616 "Date: ".strftime("%a, %d %b %Y %H:%M:%S %z",localtime)."\n",
5617 "Subject: $subj\n",
[529]5618 "X-Mailer: DNSAdmin v".$DNSDB::VERSION." Notify\n",
[517]5619 "Organization: $self->{orgname}\n",
[197]5620 "\n$message\n");
5621 $mailer->quit;
5622}
5623
[2]5624# shut Perl up
56251;
Note: See TracBrowser for help on using the repository browser.