source: trunk/DNSDB.pm@ 38

Last change on this file since 38 was 38, checked in by Kris Deugau, 15 years ago

/trunk

checkpoint - big changes!

dns.cgi and DNSDB.pm:

  • all output pages should validate as XHTML 1.0 Strict. For at least another five minutes.
  • add variable to control env dump (which doesn't validate...)
  • fiddle username add to use "uname" as username field, since username seems to cause HTML::Template to barf suddenly... O_o
  • tweak "change current group" form URI for XHTML's idiotic "all ampersands must be exscapededed ALL EVARWERE!!!one11!"
  • check for existence of group, domain, user at beginning of respective add* subs in DNSDB.pm

templates/*:

  • adjust all templates to account for oddities and stupidities of XHTML 1.0. Templates do NOT validate on their own, but the final output should. WTFisms include (but are not limited to):
    • escaped ampersands in links?
    • "<form> can't go here"?
    • <td> can't use nowrap, width?
    • "<fieldset>"?
    • and now that the form fields are enclosed in a fieldset... apparently you can't right-click in "noncontent" space "inside" that element. GRRRRRRR!!!!!
  • CSS now needs work to make things appear properly, due largely to the WTFisms above
  • all this XHTML fixing got me to also normalize and clean up inconsistent usage, general structures, and things that wouldn't even pass HTML 4.0.1 Transitional (I think) - eg missing closing tags on <tr> or <td>
  • delete raw dump of Vega's login page (need to delete a couple others)
  • Property svn:keywords set to Date Rev Author Id
File size: 30.6 KB
Line 
1# dns/trunk/DNSDB.pm
2# Abstraction functions for DNS administration
3###
4# SVN revision info
5# $Date: 2009-11-20 22:32:04 +0000 (Fri, 20 Nov 2009) $
6# SVN revision $Rev: 38 $
7# Last update by $Author: kdeugau $
8###
9# Copyright (C) 2008 - Kris Deugau <kdeugau@deepnet.cx>
10
11package DNSDB;
12
13use strict;
14use warnings;
15use Exporter;
16use DBI;
17use Net::DNS;
18#use Net::SMTP;
19#use NetAddr::IP qw( Compact );
20#use POSIX;
21use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
22
23$VERSION = 0.1;
24@ISA = qw(Exporter);
25@EXPORT_OK = qw(
26 &initGlobals &connectDB &finish
27 &addDomain &delDomain &domainName
28 &addGroup &delGroup &getChildren &groupName
29 &addUser &delUser &userFullName
30 &getSOA &getRecLine &getDomRecs
31 &addRec &updateRec &delRec
32 &domStatus &importAXFR
33 %typemap %reverse_typemap
34 );
35
36@EXPORT = (); # Export nothing by default.
37%EXPORT_TAGS = ( ALL => [qw(
38 &initGlobals &connectDB &finish
39 &addDomain &delDomain &domainName
40 &addGroup &delGroup &getChildren &groupName
41 &addUser &delUser &userFullName
42 &getSOA &getRecLine &getDomRecs
43 &addRec &updateRec &delRec
44 &domStatus &importAXFR
45 %typemap %reverse_typemap
46 )]
47 );
48
49our $group = 1;
50our $errstr = '';
51
52# Halfway sane defaults for SOA, TTL, etc.
53our %def = qw (
54 contact hostmaster.DOMAIN
55 prins ns1.myserver.com
56 soattl 86400
57 refresh 10800
58 retry 3600
59 expire 604800
60 minttl 10800
61 ttl 10800
62);
63
64# DNS record type map and reverse map.
65# loaded from the database, from http://www.iana.org/assignments/dns-parameters
66our %typemap;
67our %reverse_typemap;
68
69##
70## Initialization and cleanup subs
71##
72
73## DNSDB::connectDB()
74# Creates connection to DNS database.
75# Requires the database name, username, and password.
76# Returns a handle to the db.
77# Set up for a PostgreSQL db; could be any transactional DBMS with the
78# right changes.
79sub connectDB {
80 $errstr = '';
81 my $dbname = shift;
82 my $user = shift;
83 my $pass = shift;
84 my $dbh;
85 my $DSN = "DBI:Pg:dbname=$dbname";
86
87 my $host = shift;
88 $DSN .= ";host=$host" if $host;
89
90# Note that we want to autocommit by default, and we will turn it off locally as necessary.
91# We may not want to print gobbledygook errors; YMMV. Have to ponder that further.
92 $dbh = DBI->connect($DSN, $user, $pass, {
93 AutoCommit => 1,
94 PrintError => 0
95 })
96 or return (undef, $DBI::errstr) if(!$dbh);
97
98# Return here if we can't select. Note that this indicates a
99# problem executing the select.
100 my $sth = $dbh->prepare("select group_id from groups limit 1");
101 $sth->execute();
102 return (undef,$DBI::errstr) if ($sth->err);
103
104# See if the select returned anything (or null data). This should
105# succeed if the select executed, but...
106 $sth->fetchrow();
107 return (undef,$DBI::errstr) if ($sth->err);
108
109 $sth->finish;
110
111# If we get here, we should be OK.
112 return ($dbh,"DB connection OK");
113} # end connectDB
114
115
116## DNSDB::finish()
117# Cleans up after database handles and so on.
118# Requires a database handle
119sub finish {
120 my $dbh = $_[0];
121 $dbh->disconnect;
122} # end finish
123
124
125## DNSDB::initGlobals()
126# Initialize global variables
127# NB: this does NOT include web-specific session variables!
128# Requires a database handle
129sub initGlobals {
130 my $dbh = shift;
131
132# load system-wide site defaults and things from config file
133 if (open SYSDEFAULTS, "</etc/dnsdb.conf") {
134##fixme - error check!
135 while (<SYSDEFAULTS>) {
136 next if /^\s*#/;
137 $def{contact} = $1 if /contact ?= ?([a-z0-9_.-]+)/i;
138 $def{prins} = $1 if /prins ?= ?([a-z0-9_.-]+)/i;
139 $def{soattl} = $1 if /soattl ?= ?([a-z0-9_.-]+)/i;
140 $def{refresh} = $1 if /refresh ?= ?([a-z0-9_.-]+)/i;
141 $def{retry} = $1 if /retry ?= ?([a-z0-9_.-]+)/i;
142 $def{expire} = $1 if /expire ?= ?([a-z0-9_.-]+)/i;
143 $def{minttl} = $1 if /minttl ?= ?([a-z0-9_.-]+)/i;
144 $def{ttl} = $1 if /ttl ?= ?([a-z0-9_.-]+)/i;
145##fixme? load DB user/pass from config file?
146 }
147 }
148# load from database
149 my $sth = $dbh->prepare("select val,name from rectypes");
150 $sth->execute;
151 while (my ($recval,$recname) = $sth->fetchrow_array()) {
152 $typemap{$recval} = $recname;
153 $reverse_typemap{$recname} = $recval;
154 }
155} # end initGlobals
156
157
158##
159## Processing subs
160##
161
162## DNSDB::addDomain()
163# Add a domain
164# Takes a database handle, domain name, numeric group, and boolean(ish) state (active/inactive)
165# Returns a status code and message
166sub addDomain {
167 $errstr = '';
168 my $dbh = shift;
169 return ('FAIL',"Need database handle") if !$dbh;
170 my $domain = shift;
171 return ('FAIL',"Need domain") if !defined($domain);
172 my $group = shift;
173 return ('FAIL',"Need group") if !defined($group);
174 my $state = shift;
175 return ('FAIL',"Need domain status") if !defined($state);
176
177 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");
178 my $dom_id;
179
180# quick check to start to see if we've already got one
181 $sth->execute($domain);
182 ($dom_id) = $sth->fetchrow_array;
183
184 return ('FAIL', "Domain already exists") if $dom_id;
185
186 # Allow transactions, and raise an exception on errors so we can catch it later.
187 # Use local to make sure these get "reset" properly on exiting this block
188 local $dbh->{AutoCommit} = 0;
189 local $dbh->{RaiseError} = 1;
190
191 # Wrap all the SQL in a transaction
192 eval {
193 # insert the domain...
194 my $sth = $dbh->prepare("insert into domains (domain,group_id,status) values (?,?,?)");
195 $sth->execute($domain,$group,$state);
196
197 # get the ID...
198 $sth = $dbh->prepare("select domain_id from domains where domain='$domain'");
199 $sth->execute;
200 ($dom_id) = $sth->fetchrow_array();
201
202 # ... and now we construct the standard records from the default set. NB: group should be variable.
203 $sth = $dbh->prepare("select host,type,val,distance,weight,port,ttl from default_records where group_id=$group");
204 my $sth_in = $dbh->prepare("insert into records (domain_id,host,type,val,distance,weight,port,ttl)".
205 " values ($dom_id,?,?,?,?,?,?,?)");
206 $sth->execute;
207 while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $sth->fetchrow_array()) {
208 $host =~ s/DOMAIN/$domain/g;
209 $val =~ s/DOMAIN/$domain/g;
210 $sth_in->execute($host,$type,$val,$dist,$weight,$port,$ttl);
211 }
212
213 # once we get here, we should have suceeded.
214 $dbh->commit;
215 }; # end eval
216
217 if ($@) {
218 my $msg = $@;
219 eval { $dbh->rollback; };
220 return ('FAIL',$msg);
221 } else {
222 return ('OK',$dom_id);
223 }
224} # end addDomain
225
226
227## DNSDB::delDomain()
228# Delete a domain.
229# for now, just delete the records, then the domain.
230# later we may want to archive it in some way instead (status code 2, for example?)
231sub delDomain {
232 my $dbh = shift;
233 my $domid = shift;
234
235 # Allow transactions, and raise an exception on errors so we can catch it later.
236 # Use local to make sure these get "reset" properly on exiting this block
237 local $dbh->{AutoCommit} = 0;
238 local $dbh->{RaiseError} = 1;
239
240 my $failmsg = '';
241
242 # Wrap all the SQL in a transaction
243 eval {
244 my $sth = $dbh->prepare("delete from records where domain_id=?");
245 $failmsg = "Failure removing domain records";
246 $sth->execute($domid);
247 $sth = $dbh->prepare("delete from domains where domain_id=?");
248 $failmsg = "Failure removing domain";
249 $sth->execute($domid);
250
251 # once we get here, we should have suceeded.
252 $dbh->commit;
253 }; # end eval
254
255 if ($@) {
256 my $msg = $@;
257 eval { $dbh->rollback; };
258 return ('FAIL',"$failmsg: $msg");
259 } else {
260 return ('OK','OK');
261 }
262
263} # end delDomain()
264
265
266## DNSDB::domainName()
267# Return the domain name based on a domain ID
268# Takes a database handle and the domain ID
269# Returns the domain name or undef on failure
270sub domainName {
271 $errstr = '';
272 my $dbh = shift;
273 my $domid = shift;
274 my $sth = $dbh->prepare("select domain from domains where domain_id=?");
275 $sth->execute($domid);
276 my ($domname) = $sth->fetchrow_array();
277 $errstr = $DBI::errstr if !$domname;
278 return $domname if $domname;
279} # end domainName
280
281
282## DNSDB::addGroup()
283# Add a group
284# Takes a database handle, group name, parent group, and template-vs-cloneme flag
285# Returns a status code and message
286sub addGroup {
287 $errstr = '';
288 my $dbh = shift;
289 my $groupname = shift;
290 my $pargroup = shift;
291
292 # 0 indicates "template", hardcoded.
293 # Any other value clones that group's default records, if it exists.
294 my $torc = shift || 0;
295
296 # Allow transactions, and raise an exception on errors so we can catch it later.
297 # Use local to make sure these get "reset" properly on exiting this block
298 local $dbh->{AutoCommit} = 0;
299 local $dbh->{RaiseError} = 1;
300
301 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?");
302 my $group_id;
303
304# quick check to start to see if we've already got one
305 $sth->execute($groupname);
306 ($group_id) = $sth->fetchrow_array;
307
308 return ('FAIL', "Group already exists") if $group_id;
309
310 # Wrap all the SQL in a transaction
311 eval {
312 $sth = $dbh->prepare("INSERT INTO groups (parent_group_id,group_name) VALUES (?,?)");
313 $sth->execute($pargroup,$groupname);
314
315 $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?");
316 $sth->execute($groupname);
317 my ($groupid) = $sth->fetchrow_array();
318
319 $sth = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
320 "VALUES ($groupid,?,?,?,?,?,?,?)");
321 if ($torc) {
322 my $sth2 = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?");
323 while (my @clonedata = $sth2->fetchrow_array) {
324 $sth->execute(@clonedata);
325 }
326 } else {
327 # reasonable basic defaults for SOA, MX, NS, and minimal hosting
328 # could load from a config file, but somewhere along the line we need hardcoded bits.
329 $sth->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
330 $sth->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
331 $sth->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
332 $sth->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
333 $sth->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
334 $sth->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
335 }
336
337 # once we get here, we should have suceeded.
338 $dbh->commit;
339 }; # end eval
340
341 if ($@) {
342 my $msg = $@;
343 eval { $dbh->rollback; };
344 return ('FAIL',$msg);
345 } else {
346 return ('OK','OK');
347 }
348
349} # end addGroup()
350
351
352## DNSDB::delGroup()
353# Delete a group.
354# Takes a group ID
355# Returns a status code and message
356sub delGroup {
357 my $dbh = shift;
358 my $groupid = shift;
359
360 # Allow transactions, and raise an exception on errors so we can catch it later.
361 # Use local to make sure these get "reset" properly on exiting this block
362 local $dbh->{AutoCommit} = 0;
363 local $dbh->{RaiseError} = 1;
364
365##fixme: locate "knowable" error conditions and deal with them before the eval
366# ... or inside, whatever.
367# -> domains still exist in group
368# -> ...
369 my $failmsg = '';
370
371 # Wrap all the SQL in a transaction
372 eval {
373 my $sth = $dbh->prepare("SELECT count(*) FROM domains WHERE group_id=?");
374 $sth->execute($groupid);
375 my ($domcnt) = $sth->fetchrow_array;
376 $failmsg = "Can't remove group ".groupName($dbh,$groupid);
377 die "$domcnt domains still in group\n" if $domcnt;
378
379 $sth = $dbh->prepare("delete from default_records where group_id=?");
380 $failmsg = "Failed to delete default records for ".groupName($dbh,$groupid);
381 $sth->execute($groupid);
382 $sth = $dbh->prepare("delete from groups where group_id=?");
383 $failmsg = "Failed to remove group ".groupName($dbh,$groupid);
384 $sth->execute($groupid);
385
386 # once we get here, we should have suceeded.
387 $dbh->commit;
388 }; # end eval
389
390 if ($@) {
391 my $msg = $@;
392 eval { $dbh->rollback; };
393 return ('FAIL',"$failmsg: $msg");
394 } else {
395 return ('OK','OK');
396 }
397} # end delGroup()
398
399
400## DNSDB::getChildren()
401# Get a list of all groups whose parent^n is group <n>
402# Takes a database handle, group ID, reference to an array to put the group IDs in,
403# and an optional flag to return only immediate children or all children-of-children
404# default to returning all children
405# Calls itself
406sub getChildren {
407 $errstr = '';
408 my $dbh = shift;
409 my $rootgroup = shift;
410 my $groupdest = shift;
411 my $immed = shift || 'all';
412
413 # special break for default group; otherwise we get stuck.
414 if ($rootgroup == 1) {
415 # by definition, group 1 is the Root Of All Groups
416 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE NOT (group_id=1)".
417 ($immed ne 'all' ? " AND parent_group_id=1" : ''));
418 $sth->execute;
419 while (my @this = $sth->fetchrow_array) {
420 push @$groupdest, @this;
421 }
422 } else {
423 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE parent_group_id=?");
424 $sth->execute($rootgroup);
425 return if $sth->rows == 0;
426 my @grouplist;
427 while (my ($group) = $sth->fetchrow_array) {
428 push @$groupdest, $group;
429 getChildren($dbh,$group,$groupdest) if $immed eq 'all';
430 }
431 }
432} # end getChildren()
433
434
435## DNSDB::groupName()
436# Return the group name based on a group ID
437# Takes a database handle and the group ID
438# Returns the group name or undef on failure
439sub groupName {
440 $errstr = '';
441 my $dbh = shift;
442 my $groupid = shift;
443 my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?");
444 $sth->execute($groupid);
445 my ($groupname) = $sth->fetchrow_array();
446 $errstr = $DBI::errstr if !$groupname;
447 return $groupname if $groupname;
448} # end groupName
449
450
451## DNSDB::addUser()
452#
453sub addUser {
454 $errstr = '';
455 my $dbh = shift;
456 return ('FAIL',"Need database handle") if !$dbh;
457 my $username = shift;
458 return ('FAIL',"Missing username") if !defined($username);
459 my $group = shift;
460 return ('FAIL',"Missing group") if !defined($group);
461 my $pass = shift;
462 return ('FAIL',"Missing password") if !defined($pass);
463 my $state = shift;
464 return ('FAIL',"Need account status") if !defined($state);
465
466 my $type = shift || 'u'; # create limited users by default - fwiw, not sure yet how this will interact with ACLs
467
468 my $fname = shift || $username;
469 my $lname = shift || '';
470 my $phone = shift || ''; # not going format-check
471
472 my $sth = $dbh->prepare("SELECT user_id FROM users WHERE username=?");
473 my $user_id;
474
475# quick check to start to see if we've already got one
476 $sth->execute($username);
477 ($user_id) = $sth->fetchrow_array;
478
479 return ('FAIL', "User already exists") if $user_id;
480
481 # Allow transactions, and raise an exception on errors so we can catch it later.
482 # Use local to make sure these get "reset" properly on exiting this block
483 local $dbh->{AutoCommit} = 0;
484 local $dbh->{RaiseError} = 1;
485
486 # Wrap all the SQL in a transaction
487 eval {
488 # insert the user...
489 my $sth = $dbh->prepare("INSERT INTO users (group_id,username,password,firstname,lastname,phone,type,status) ".
490 "VALUES (?,?,?,?,?,?,?,?)");
491 $sth->execute($group,$username,$pass,$fname,$lname,$phone,$type,$state);
492
493 # get the ID...
494 $sth = $dbh->prepare("select user_id from users where username=?");
495 $sth->execute($username);
496 ($user_id) = $sth->fetchrow_array();
497
498##fixme: add another table to hold name/email for log table?
499
500 # once we get here, we should have suceeded.
501 $dbh->commit;
502 }; # end eval
503
504 if ($@) {
505 my $msg = $@;
506 eval { $dbh->rollback; };
507 return ('FAIL',$msg);
508 } else {
509 return ('OK',$user_id);
510 }
511} # end addUser
512
513
514## DNSDB::delUser()
515#
516sub delUser {
517 my $dbh = shift;
518 return ('FAIL',"Need database handle") if !$dbh;
519 my $userid = shift;
520 return ('FAIL',"Missing userid") if !defined($userid);
521
522 my $sth = $dbh->prepare("delete from users where user_id=?");
523 $sth->execute($userid);
524
525 return ('FAIL',"Couldn't remove user: ".$sth->errstr) if $sth->err;
526
527 return ('OK','OK');
528
529} # end delUser
530
531
532## DNSDB::userFullName()
533# Return a pretty string!
534# Takes a user_id and optional printf-ish string to indicate which pieces where:
535# %u for the username
536# %f for the first name
537# %l for the last name
538# All other text in the passed string will be left as-is.
539##fixme: need a "smart" option too, so that missing/null/blank first/last names don't give funky output
540sub userFullName {
541 $errstr = '';
542 my $dbh = shift;
543 my $userid = shift;
544 my $fullformat = shift || '%f %l (%u)';
545 my $sth = $dbh->prepare("select username,firstname,lastname from users where user_id=?");
546 $sth->execute($userid);
547 my ($uname,$fname,$lname) = $sth->fetchrow_array();
548 $errstr = $DBI::errstr if !$uname;
549
550 $fullformat =~ s/\%u/$uname/g;
551 $fullformat =~ s/\%f/$fname/g;
552 $fullformat =~ s/\%l/$lname/g;
553
554 return $fullformat;
555} # end userFullName
556
557
558## DNSDB::editRecord()
559# Change an existing record
560# Takes a database handle, default/live flag, record ID, and new data and updates the data fields for it
561sub editRecord {
562 $errstr = '';
563 my $dbh = shift;
564 my $defflag = shift;
565 my $recid = shift;
566 my $host = shift;
567 my $address = shift;
568 my $distance = shift;
569 my $weight = shift;
570 my $port = shift;
571 my $ttl = shift;
572}
573
574
575## DNSDB::getSOA()
576# Return all suitable fields from an SOA record in separate elements of a hash
577# Takes a database handle, default/live flag, and group (default) or domain (live) ID
578sub getSOA {
579 $errstr = '';
580 my $dbh = shift;
581 my $def = shift;
582 my $id = shift;
583 my %ret;
584
585 my $sql = "select record_id,host,val,ttl from";
586 if ($def eq 'def' or $def eq 'y') {
587 $sql .= " default_records where group_id=$id and type=$reverse_typemap{SOA}";
588 } else {
589 # we're editing a live SOA record; find based on domain
590 $sql .= " records where domain_id=$id and type=$reverse_typemap{SOA}";
591 }
592 my $sth = $dbh->prepare($sql);
593 $sth->execute;
594
595 my ($recid,$host,$val,$ttl) = $sth->fetchrow_array();
596 my ($prins,$contact) = split /:/, $host;
597 my ($refresh,$retry,$expire,$minttl) = split /:/, $val;
598
599 $ret{recid} = $recid;
600 $ret{ttl} = $ttl;
601 $ret{prins} = $prins;
602 $ret{contact} = $contact;
603 $ret{refresh} = $refresh;
604 $ret{retry} = $retry;
605 $ret{expire} = $expire;
606 $ret{minttl} = $minttl;
607
608 return %ret;
609} # end getSOA()
610
611
612## DNSDB::getRecLine()
613# Return all data fields for a zone record in separate elements of a hash
614# Takes a database handle, default/live flag, and record ID
615sub getRecLine {
616 $errstr = '';
617 my $dbh = shift;
618 my $def = shift;
619 my $id = shift;
620
621 my $sql = "select record_id,host,type,val,distance,weight,port,ttl from ".
622 (($def eq 'def' or $def eq 'y') ? 'default_' : '').
623 "records where record_id=$id";
624print "MDEBUG: $sql<br>\n";
625 my $sth = $dbh->prepare($sql);
626 $sth->execute;
627
628 my ($recid,$host,$rtype,$val,$distance,$weight,$port,$ttl) = $sth->fetchrow_array();
629
630 if ($sth->err) {
631 $errstr = $DBI::errstr;
632 return undef;
633 }
634 my %ret;
635 $ret{recid} = $recid;
636 $ret{host} = $host;
637 $ret{type} = $rtype;
638 $ret{val} = $val;
639 $ret{distance}= $distance;
640 $ret{weight} = $weight;
641 $ret{port} = $port;
642 $ret{ttl} = $ttl;
643
644 return %ret;
645}
646
647
648##fixme: should use above (getRecLine()) to get lines for below?
649## DNSDB::getDomRecs()
650# Return records for a domain
651# Takes a database handle, default/live flag, group/domain ID, start,
652# number of records, sort field, and sort order
653# Returns a reference to an array of hashes
654sub getDomRecs {
655 $errstr = '';
656 my $dbh = shift;
657 my $type = shift;
658 my $id = shift;
659 my $nrecs = shift || 'all';
660 my $nstart = shift || 0;
661
662## for order, need to map input to column names
663 my $order = shift || 'host';
664
665 my $sql = "select record_id,host,type,val,distance,weight,port,ttl from";
666 if ($type eq 'def' or $type eq 'y') {
667 $sql .= " default_records where group_id=$id";
668 } else {
669 $sql .= " records where domain_id=$id";
670 }
671 $sql .= " and not type=$reverse_typemap{SOA} order by $order";
672##fixme: need to set nstart properly (offset is not internally multiplied with limit)
673 $sql .= " limit $nrecs offset ".($nstart*$nrecs) if $nstart ne 'all';
674
675 my $sth = $dbh->prepare($sql);
676 $sth->execute;
677
678 my @retbase;
679 while (my $ref = $sth->fetchrow_hashref()) {
680 push @retbase, $ref;
681 }
682
683 my $ret = \@retbase;
684 return $ret;
685} # end getDomRecs()
686
687
688## DNSDB::addRec()
689# Add a new record to a domain or a group's default records
690# Takes a database handle, default/live flag, group/domain ID,
691# host, type, value, and TTL
692# Some types require additional detail: "distance" for MX and SRV,
693# and weight/port for SRV
694# Returns a status code and detail message in case of error
695sub addRec {
696 $errstr = '';
697 my $dbh = shift;
698 my $defrec = shift;
699 my $id = shift;
700
701 my $host = shift;
702 my $rectype = shift;
703 my $val = shift;
704 my $ttl = shift;
705
706 my $fields = ($defrec eq 'y' ? 'group_id' : 'domain_id').",host,type,val,ttl";
707 my $vallen = "?,?,?,?,?";
708 my @vallist = ($id,$host,$rectype,$val,$ttl);
709
710 my $dist;
711 if ($rectype == $reverse_typemap{MX} or $rectype == $reverse_typemap{SRV}) {
712 $dist = shift;
713 return ('FAIL',"Need distance for $typemap{$rectype} record") if !defined($dist);
714 $fields .= ",distance";
715 $vallen .= ",?";
716 push @vallist, $dist;
717 }
718 my $weight;
719 my $port;
720 if ($rectype == $reverse_typemap{SRV}) {
721 # check for _service._protocol. NB: RFC2782 does not say "MUST"... nor "SHOULD"...
722 # it just says (paraphrased) "... is prepended with _ to prevent DNS collisions"
723 return ('FAIL',"SRV records must begin with _service._protocol")
724 if $host !~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-z0-9-]+/;
725 $weight = shift;
726 $port = shift;
727 return ('FAIL',"Need weight and port for SRV record") if !defined($weight) or !defined($port);
728 $fields .= ",weight,port";
729 $vallen .= ",?,?";
730 push @vallist, ($weight,$port);
731 }
732
733 my $sql = "insert into ".($defrec eq 'y' ? 'default_' : '')."records ($fields) values ($vallen)";
734##fixme: use array for values, replace "vallist" with series of ?,?,? etc
735# something is bugging me about this...
736#warn "DEBUG: $sql";
737 my $sth = $dbh->prepare($sql);
738 $sth->execute(@vallist);
739
740 return ('FAIL',$sth->errstr) if $sth->err;
741
742 return ('OK','OK');
743} # end addRec()
744
745
746## DNSDB::updateRec()
747# Update a record
748sub updateRec {
749 $errstr = '';
750
751 my $dbh = shift;
752 my $defrec = shift;
753 my $id = shift;
754
755# all records have these
756 my $host = shift;
757 my $type = shift;
758 my $val = shift;
759 my $ttl = shift;
760
761 return('FAIL',"Missing standard argument(s)") if !defined($ttl);
762
763# only MX and SRV will use these
764 my $dist = 0;
765 my $weight = 0;
766 my $port = 0;
767
768 if ($type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) {
769 $dist = shift;
770 return ('FAIL',"MX or SRV requires distance") if !defined($dist);
771 if ($type == $reverse_typemap{SRV}) {
772 $weight = shift;
773 return ('FAIL',"SRV requires weight") if !defined($weight);
774 $port = shift;
775 return ('FAIL',"SRV requires port") if !defined($port);
776 }
777 }
778
779 my $sth = $dbh->prepare("UPDATE ".($defrec eq 'y' ? 'default_' : '')."records ".
780 "SET host=?,type=?,val=?,ttl=?,distance=?,weight=?,port=? ".
781 "WHERE record_id=?");
782 $sth->execute($host,$type,$val,$ttl,$dist,$weight,$port,$id);
783
784 return ('FAIL',$sth->errstr."<br>\n$errstr<br>\n") if $sth->err;
785
786 return ('OK','OK');
787} # end updateRec()
788
789
790## DNSDB::delRec()
791# Delete a record.
792sub delRec {
793 $errstr = '';
794 my $dbh = shift;
795 my $defrec = shift;
796 my $id = shift;
797
798 my $sth = $dbh->prepare("delete from ".($defrec eq 'y' ? 'default_' : '')."records where record_id=?");
799 $sth->execute($id);
800
801 return ('FAIL',"Couldn't remove record: ".$sth->errstr) if $sth->err;
802
803 return ('OK','OK');
804} # end delRec()
805
806
807## DNSDB::domStatus()
808# Sets and/or returns a domain's status
809# Takes a database handle, domain ID and optionally a status argument
810# Returns undef on errors.
811sub domStatus {
812 my $dbh = shift;
813 my $id = shift;
814 my $newstatus = shift;
815
816 return undef if $id !~ /^\d+$/;
817
818 my $sth;
819
820# ooo, fun! let's see what we were passed for status
821 if ($newstatus) {
822 $sth = $dbh->prepare("update domains set status=? where domain_id=?");
823 # ass-u-me caller knows what's going on in full
824 if ($newstatus =~ /^[01]$/) { # only two valid for now.
825 $sth->execute($newstatus,$id);
826 } elsif ($newstatus =~ /^domo(?:n|ff)$/) {
827 $sth->execute(($newstatus eq 'domon' ? 1 : 0),$id);
828 }
829 }
830
831 $sth = $dbh->prepare("select status from domains where domain_id=?");
832 $sth->execute($id);
833 my ($status) = $sth->fetchrow_array;
834 return $status;
835} # end domStatus()
836
837
838## DNSDB::importAXFR
839# Import a domain via AXFR
840# Takes AXFR host, domain to transfer, group to put the domain in,
841# and optionally:
842# - active/inactive state flag (defaults to active)
843# - overwrite-SOA flag (defaults to off)
844# - overwrite-NS flag (defaults to off, doesn't affect subdomain NS records)
845# Returns a status code (OK, WARN, or FAIL) and message - message should be blank
846# if status is OK, but WARN includes conditions that are not fatal but should
847# really be reported.
848sub importAXFR {
849 my $dbh = shift;
850 my $ifrom_in = shift;
851 my $domain = shift;
852 my $group = shift;
853 my $status = shift || 1;
854 my $rwsoa = shift || 0;
855 my $rwns = shift || 0;
856
857##fixme: add mode to delete&replace, merge+overwrite, merge new?
858
859 my $nrecs = 0;
860 my $soaflag = 0;
861 my $nsflag = 0;
862 my $warnmsg = '';
863 my $ifrom;
864
865 # choke on possible bad setting in ifrom
866 # IPv4 and v6, and valid hostnames!
867 ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i);
868 return ('FAIL', "Bad AXFR source host $ifrom")
869 unless ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i);
870
871 # Allow transactions, and raise an exception on errors so we can catch it later.
872 # Use local to make sure these get "reset" properly on exiting this block
873 local $dbh->{AutoCommit} = 0;
874 local $dbh->{RaiseError} = 1;
875
876 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");
877 my $dom_id;
878
879# quick check to start to see if we've already got one
880 $sth->execute($domain);
881 ($dom_id) = $sth->fetchrow_array;
882
883 return ('FAIL', "Domain already exists") if $dom_id;
884
885 eval {
886 # can't do this, can't nest transactions. sigh.
887 #my ($dcode, $dmsg) = addDomain(dbh, domain, group, status);
888
889##fixme: serial
890 my $sth = $dbh->prepare("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)");
891 $sth->execute($domain,$group,$status);
892
893## bizarre DBI<->Net::DNS interaction bug:
894## sometimes a zone will cause an immediate commit-and-exit (sort of) of the while()
895## fixed, apparently I was doing *something* odd, but not certain what it was that
896## caused a commit instead of barfing
897
898 # get domain id so we can do the records
899 $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");
900 $sth->execute($domain);
901 ($dom_id) = $sth->fetchrow_array();
902
903 my $res = Net::DNS::Resolver->new;
904 $res->nameservers($ifrom);
905 $res->axfr_start($domain)
906 or die "Couldn't begin AXFR\n";
907
908 while (my $rr = $res->axfr_next()) {
909 my $type = $rr->type;
910
911 my $sql = "INSERT INTO records (domain_id,host,type,ttl,val";
912 my $vallen = "?,?,?,?,?";
913
914 $soaflag = 1 if $type eq 'SOA';
915 $nsflag = 1 if $type eq 'NS';
916
917 my @vallist = ($dom_id, $rr->name, $reverse_typemap{$type}, $rr->ttl);
918
919# "Primary" types:
920# A, NS, CNAME, SOA, PTR(warn in forward), MX, TXT, AAAA, SRV, A6(ob), SPF
921# maybe KEY
922
923# nasty big ugly case-like thing here, since we have to do *some* different
924# processing depending on the record. le sigh.
925
926 if ($type eq 'A') {
927 push @vallist, $rr->address;
928 } elsif ($type eq 'NS') {
929# hmm. should we warn here if subdomain NS'es are left alone?
930 next if ($rwns && ($rr->name eq $domain));
931 push @vallist, $rr->nsdname;
932 $nsflag = 1;
933 } elsif ($type eq 'CNAME') {
934 push @vallist, $rr->cname;
935 } elsif ($type eq 'SOA') {
936 next if $rwsoa;
937 $vallist[1] = $rr->mname.":".$rr->rname;
938 push @vallist, ($rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum);
939 $soaflag = 1;
940 } elsif ($type eq 'PTR') {
941 # hmm. PTR records should not be in forward zones.
942 } elsif ($type eq 'MX') {
943 $sql .= ",distance";
944 $vallen .= ",?";
945 push @vallist, $rr->exchange;
946 push @vallist, $rr->preference;
947 } elsif ($type eq 'TXT') {
948##fixme: Net::DNS docs say this should be deprecated for rdatastr() or char_str_list(),
949## but don't really seem enthusiastic about it.
950 push @vallist, $rr->txtdata;
951 } elsif ($type eq 'SPF') {
952##fixme: and the same caveat here, since it is apparently a clone of ::TXT
953 push @vallist, $rr->txtdata;
954 } elsif ($type eq 'AAAA') {
955 push @vallist, $rr->address;
956 } elsif ($type eq 'SRV') {
957 $sql .= ",distance,weight,port" if $type eq 'SRV';
958 $vallen .= ",?,?,?" if $type eq 'SRV';
959 push @vallist, $rr->target;
960 push @vallist, $rr->priority;
961 push @vallist, $rr->weight;
962 push @vallist, $rr->port;
963 } elsif ($type eq 'KEY') {
964 # we don't actually know what to do with these...
965 push @vallist, ($rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname);
966 } else {
967 push @vallist, $rr->rdatastr;
968 # Finding a different record type is not fatal.... just problematic.
969 # We may not be able to export it correctly.
970 $warnmsg .= "Unusual record ".$rr->name." ($type) found\n";
971 }
972
973# BIND supports:
974# A CNAME HINFO MB(ex) MD(ob) MF(ob) MG(ex) MINFO(ex) MR(ex) MX NS NULL
975# PTR SOA TXT WKS AFSDB(ex) ISDN(ex) RP(ex) RT(ex) X25(ex) PX
976# ... if one can ever find the right magic to format them correctly
977
978# Net::DNS supports:
979# RRSIG SIG NSAP NS NIMLOC NAPTR MX MR MINFO MG MB LOC ISDN IPSECKEY HINFO
980# EID DNAME CNAME CERT APL AFSDB AAAA A DS NXT NSEC3PARAM NSEC3 NSEC KEY
981# DNSKEY DLV X25 TXT TSIG TKEY SSHFP SRV SPF SOA RT RP PX PTR NULL APL::AplItem
982
983 $sth = $dbh->prepare($sql.") VALUES (".$vallen.")") or die "problem preparing record insert SQL\n";
984 $sth->execute(@vallist) or die "failed to insert ".$rr->string.": ".$sth->errstr."\n";
985
986 $nrecs++;
987
988 } # while axfr_next
989
990 # Overwrite SOA record
991 if ($rwsoa) {
992 $soaflag = 1;
993 my $sthgetsoa = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
994 my $sthputsoa = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)");
995 $sthgetsoa->execute($group,$reverse_typemap{SOA});
996 while (my ($host,$val,$ttl) = $sthgetsoa->fetchrow_array()) {
997 $host =~ s/DOMAIN/$domain/g;
998 $val =~ s/DOMAIN/$domain/g;
999 $sthputsoa->execute($dom_id,$host,$reverse_typemap{SOA},$val,$ttl);
1000 }
1001 }
1002
1003 # Overwrite NS records
1004 if ($rwns) {
1005 $nsflag = 1;
1006 my $sthgetns = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
1007 my $sthputns = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)");
1008 $sthgetns->execute($group,$reverse_typemap{NS});
1009 while (my ($host,$val,$ttl) = $sthgetns->fetchrow_array()) {
1010 $host =~ s/DOMAIN/$domain/g;
1011 $val =~ s/DOMAIN/$domain/g;
1012 $sthputns->execute($dom_id,$host,$reverse_typemap{NS},$val,$ttl);
1013 }
1014 }
1015
1016 die "No records found; either $ifrom is not authoritative or doesn't allow transfers\n" if !$nrecs;
1017 die "Bad zone: No SOA record!\n" if !$soaflag;
1018 die "Bad zone: No NS records!\n" if !$nsflag;
1019
1020 $dbh->commit;
1021
1022 };
1023
1024 if ($@) {
1025 my $msg = $@;
1026 eval { $dbh->rollback; };
1027 return ('FAIL',$msg." $warnmsg");
1028 } else {
1029 return ('WARN', $warnmsg) if $warnmsg;
1030 return ('OK',"ook");
1031 }
1032
1033 # it should be impossible to get here.
1034 return ('WARN',"OOOK!");
1035} # end importAXFR()
1036
1037
1038# shut Perl up
10391;
Note: See TracBrowser for help on using the repository browser.