source: trunk/DNSDB.pm@ 130

Last change on this file since 130 was 130, checked in by Kris Deugau, 13 years ago

/trunk

Duhhhhh... "text" fields don't have any performance penalty
over varchar or char. Rip out the longrecs business in favour
of much simpler record tables. Reverts addition of same from
r90.

  • Property svn:keywords set to Date Rev Author Id
File size: 58.8 KB
Line 
1# dns/trunk/DNSDB.pm
2# Abstraction functions for DNS administration
3###
4# SVN revision info
5# $Date: 2011-10-04 14:34:18 +0000 (Tue, 04 Oct 2011) $
6# SVN revision $Rev: 130 $
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;
18use Crypt::PasswdMD5;
19#use Net::SMTP;
20#use NetAddr::IP qw( Compact );
21#use POSIX;
22use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
23
24$VERSION = 0.1;
25@ISA = qw(Exporter);
26@EXPORT_OK = qw(
27 &initGlobals
28 &initPermissions &getPermissions &changePermissions &comparePermissions
29 &changeGroup
30 &loadConfig &connectDB &finish
31 &addDomain &delDomain &domainName &domainID
32 &addGroup &delGroup &getChildren &groupName
33 &addUser &updateUser &delUser &userFullName &userStatus &getUserData
34 &getSOA &getRecLine &getDomRecs &getRecCount
35 &addRec &updateRec &delRec
36 &getParents
37 &isParent
38 &domStatus &importAXFR
39 &export
40 %typemap %reverse_typemap %config
41 %permissions @permtypes $permlist
42 );
43
44@EXPORT = (); # Export nothing by default.
45%EXPORT_TAGS = ( ALL => [qw(
46 &initGlobals
47 &initPermissions &getPermissions &changePermissions &comparePermissions
48 &changeGroup
49 &loadConfig &connectDB &finish
50 &addDomain &delDomain &domainName &domainID
51 &addGroup &delGroup &getChildren &groupName
52 &addUser &updateUser &delUser &userFullName &userStatus &getUserData
53 &getSOA &getRecLine &getDomRecs &getRecCount
54 &addRec &updateRec &delRec
55 &getParents
56 &isParent
57 &domStatus &importAXFR
58 &export
59 %typemap %reverse_typemap %config
60 %permissions @permtypes $permlist
61 )]
62 );
63
64our $group = 1;
65our $errstr = '';
66
67# Halfway sane defaults for SOA, TTL, etc.
68# serial defaults to 0 for convenience.
69# value will be either YYYYMMDDNN for BIND/etc, or auto-internal for tinydns
70our %def = qw (
71 contact hostmaster.DOMAIN
72 prins ns1.myserver.com
73 serial 0
74 soattl 86400
75 refresh 10800
76 retry 3600
77 expire 604800
78 minttl 10800
79 ttl 10800
80);
81
82# Arguably defined wholly in the db, but little reason to change without supporting code changes
83our @permtypes = qw (
84 group_edit group_create group_delete
85 user_edit user_create user_delete
86 domain_edit domain_create domain_delete
87 record_edit record_create record_delete
88 self_edit admin
89);
90our $permlist = join(',',@permtypes);
91
92# DNS record type map and reverse map.
93# loaded from the database, from http://www.iana.org/assignments/dns-parameters
94our %typemap;
95our %reverse_typemap;
96
97our %permissions;
98
99# Prepopulate a basic config. Note some of these *will* cause errors if left unset.
100our %config = (
101 # Database connection info
102 dbname => 'dnsdb',
103 dbuser => 'dnsdb',
104 dbpass => 'secret',
105 dbhost => '',
106
107 # Email notice settings
108 mailhost => 'smtp.example.com',
109 mailsender => 'dnsdb@example.com',
110 mailname => 'DNS Administration',
111
112 # Template directory
113 templatedir => 'templates/',
114# fmeh. this is a real web path, not a logical internal one. hm..
115# cssdir => 'templates/';
116 );
117
118
119##
120## Initialization and cleanup subs
121##
122
123
124## DNSDB::loadConfig()
125# Load the minimum required initial state (DB connect info) from a config file
126# Load misc other bits while we're at it.
127# Takes an optional basename and config path to look for
128# Populates the %config and %def hashes
129sub loadConfig {
130 my $basename = shift || ''; # this will work OK
131
132 my $deferr = ''; # place to put error from default config file in case we can't find either one
133
134 my $configroot = '/etc/dnsdb';
135 $configroot = '' if $basename =~ m|^/|;
136 $basename .= ".conf" if $basename !~ /\.conf$/;
137 my $defconfig = "$configroot/dnsdb.conf";
138 my $siteconfig = "$configroot/$basename";
139
140 # System defaults
141 __cfgload("$configroot/dnsdb.conf") or $deferr = $errstr;
142
143 # Per-site-ish settings
144 unless (__cfgload("$configroot/$basename")) {
145 $errstr = ($deferr ? "Error opening default config file $defconfig: $deferr\n" : '').
146 "Error opening site config file $siteconfig";
147 return;
148 }
149
150 # All good, clear the error and go home.
151 $errstr = '';
152 return 1;
153} # end loadConfig()
154
155
156## DNSDB::__cfgload()
157# Private sub to parse a config file and load it into %config
158# Takes a file handle on an open config file
159sub __cfgload {
160 $errstr = '';
161 my $cfgfile = shift;
162 if (open CFG, "<$cfgfile") {
163# my $mode = '';
164 while (<CFG>) {
165 chomp;
166 s/^\s*//;
167 next if /^#/;
168 next if /^$/;
169# hmm. more complex bits in this file might require [heading] headers, maybe?
170# $mode = $1 if /^\[(a-z)+]/;
171 # DB connect info
172 $config{dbname} = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i;
173 $config{dbuser} = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i;
174 $config{dbpass} = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i;
175 $config{dbhost} = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i;
176 # SOA defaults
177 $def{contact} = $1 if /^contact\s*=\s*([a-z0-9_.-]+)/i;
178 $def{prins} = $1 if /^prins\s*=\s*([a-z0-9_.-]+)/i;
179 $def{soattl} = $1 if /^soattl\s*=\s*([a-z0-9_.-]+)/i;
180 $def{refresh} = $1 if /^refresh\s*=\s*([a-z0-9_.-]+)/i;
181 $def{retry} = $1 if /^retry\s*=\s*([a-z0-9_.-]+)/i;
182 $def{expire} = $1 if /^expire\s*=\s*([a-z0-9_.-]+)/i;
183 $def{minttl} = $1 if /^minttl\s*=\s*([a-z0-9_.-]+)/i;
184 $def{ttl} = $1 if /^ttl\s*=\s*([a-z0-9_.-]+)/i;
185 # Mail settings
186 $config{mailhost} = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i;
187 $config{mailsender} = $1 if /^mailsender\s*=\s*([a-z0-9_.@-]+)/i;
188 $config{mailname} = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i;
189 }
190 close CFG;
191 } else {
192 $errstr = $!;
193 return;
194 }
195 return 1;
196} # end __cfgload()
197
198
199## DNSDB::connectDB()
200# Creates connection to DNS database.
201# Requires the database name, username, and password.
202# Returns a handle to the db.
203# Set up for a PostgreSQL db; could be any transactional DBMS with the
204# right changes.
205sub connectDB {
206 $errstr = '';
207 my $dbname = shift;
208 my $user = shift;
209 my $pass = shift;
210 my $dbh;
211 my $DSN = "DBI:Pg:dbname=$dbname";
212
213 my $host = shift;
214 $DSN .= ";host=$host" if $host;
215
216# Note that we want to autocommit by default, and we will turn it off locally as necessary.
217# We may not want to print gobbledygook errors; YMMV. Have to ponder that further.
218 $dbh = DBI->connect($DSN, $user, $pass, {
219 AutoCommit => 1,
220 PrintError => 0
221 })
222 or return (undef, $DBI::errstr) if(!$dbh);
223
224# Return here if we can't select. Note that this indicates a
225# problem executing the select.
226 my $sth = $dbh->prepare("select group_id from groups limit 1");
227 $sth->execute();
228 return (undef,$DBI::errstr) if ($sth->err);
229
230# See if the select returned anything (or null data). This should
231# succeed if the select executed, but...
232 $sth->fetchrow();
233 return (undef,$DBI::errstr) if ($sth->err);
234
235 $sth->finish;
236
237# If we get here, we should be OK.
238 return ($dbh,"DB connection OK");
239} # end connectDB
240
241
242## DNSDB::finish()
243# Cleans up after database handles and so on.
244# Requires a database handle
245sub finish {
246 my $dbh = $_[0];
247 $dbh->disconnect;
248} # end finish
249
250
251## DNSDB::initGlobals()
252# Initialize global variables
253# NB: this does NOT include web-specific session variables!
254# Requires a database handle
255sub initGlobals {
256 my $dbh = shift;
257
258# load system-wide site defaults and things from config file
259 if (open SYSDEFAULTS, "</etc/dnsdb.conf") {
260##fixme - error check!
261 while (<SYSDEFAULTS>) {
262 next if /^\s*#/;
263 $def{contact} = $1 if /contact ?= ?([a-z0-9_.-]+)/i;
264 $def{prins} = $1 if /prins ?= ?([a-z0-9_.-]+)/i;
265 $def{soattl} = $1 if /soattl ?= ?([a-z0-9_.-]+)/i;
266 $def{refresh} = $1 if /refresh ?= ?([a-z0-9_.-]+)/i;
267 $def{retry} = $1 if /retry ?= ?([a-z0-9_.-]+)/i;
268 $def{expire} = $1 if /expire ?= ?([a-z0-9_.-]+)/i;
269 $def{minttl} = $1 if /minttl ?= ?([a-z0-9_.-]+)/i;
270 $def{ttl} = $1 if /ttl ?= ?([a-z0-9_.-]+)/i;
271##fixme? load DB user/pass from config file?
272 }
273 }
274# load from database
275 my $sth = $dbh->prepare("select val,name from rectypes");
276 $sth->execute;
277 while (my ($recval,$recname) = $sth->fetchrow_array()) {
278 $typemap{$recval} = $recname;
279 $reverse_typemap{$recname} = $recval;
280 }
281} # end initGlobals
282
283
284## DNSDB::initPermissions()
285# Set up permissions global
286# Takes database handle and UID
287sub initPermissions {
288 my $dbh = shift;
289 my $uid = shift;
290
291# %permissions = $(getPermissions($dbh,'user',$uid));
292 getPermissions($dbh, 'user', $uid, \%permissions);
293
294} # end initPermissions()
295
296
297## DNSDB::getPermissions()
298# Get permissions from DB
299# Requires DB handle, group or user flag, ID, and hashref.
300sub getPermissions {
301 my $dbh = shift;
302 my $type = shift;
303 my $id = shift;
304 my $hash = shift;
305
306 my $sql = qq(
307 SELECT
308 p.admin,p.self_edit,
309 p.group_create,p.group_edit,p.group_delete,
310 p.user_create,p.user_edit,p.user_delete,
311 p.domain_create,p.domain_edit,p.domain_delete,
312 p.record_create,p.record_edit,p.record_delete
313 FROM permissions p
314 );
315 if ($type eq 'group') {
316 $sql .= qq(
317 JOIN groups g ON g.permission_id=p.permission_id
318 WHERE g.group_id=?
319 );
320 } else {
321 $sql .= qq(
322 JOIN users u ON u.permission_id=p.permission_id
323 WHERE u.user_id=?
324 );
325 }
326
327 my $sth = $dbh->prepare($sql);
328
329 $sth->execute($id) or die "argh: ".$sth->errstr;
330
331# my $permref = $sth->fetchrow_hashref;
332# return $permref;
333# $hash = $permref;
334# Eww. Need to learn how to forcibly drop a hashref onto an existing hash.
335 ($hash->{admin},$hash->{self_edit},
336 $hash->{group_create},$hash->{group_edit},$hash->{group_delete},
337 $hash->{user_create},$hash->{user_edit},$hash->{user_delete},
338 $hash->{domain_create},$hash->{domain_edit},$hash->{domain_delete},
339 $hash->{record_create},$hash->{record_edit},$hash->{record_delete})
340 = $sth->fetchrow_array;
341
342} # end getPermissions()
343
344
345## DNSDB::changePermissions()
346# Update an ACL entry
347# Takes a db handle, type, owner-id, and hashref for the changed permissions.
348sub changePermissions {
349 my $dbh = shift;
350 my $type = shift;
351 my $id = shift;
352 my $newperms = shift;
353 my $inherit = shift || 0;
354
355 my $failmsg = '';
356
357 # see if we're switching from inherited to custom. for bonus points,
358 # snag the permid and parent permid anyway, since we'll need the permid
359 # to set/alter custom perms, and both if we're switching from custom to
360 # inherited.
361 my $sth = $dbh->prepare("SELECT (u.permission_id=g.permission_id) AS was_inherited,u.permission_id,g.permission_id".
362 " FROM ".($type eq 'user' ? 'users' : 'groups')." u ".
363 " JOIN groups g ON u.".($type eq 'user' ? '' : 'parent_')."group_id=g.group_id ".
364 " WHERE u.".($type eq 'user' ? 'user' : 'group')."_id=?");
365 $sth->execute($id);
366
367 my ($wasinherited,$permid,$parpermid) = $sth->fetchrow_array;
368
369# hack phtoui
370# group id 1 is "special" in that it's it's own parent (err... possibly.)
371# may make its parent id 0 which doesn't exist, and as a bonus is Perl-false.
372 $wasinherited = 0 if ($type eq 'group' && $id == 1);
373
374 local $dbh->{AutoCommit} = 0;
375 local $dbh->{RaiseError} = 1;
376
377 # Wrap all the SQL in a transaction
378 eval {
379 if ($inherit) {
380
381 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='t',permission_id=? ".
382 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($parpermid, $id) );
383 $dbh->do("DELETE FROM permissions WHERE permission_id=?", undef, ($permid) );
384
385 } else {
386
387 if ($wasinherited) { # munge new permission entry in if we're switching from inherited perms
388##fixme: need to add semirecursive bit to properly munge inherited permission ID on subgroups and users
389# ... if'n'when we have groups with fully inherited permissions.
390 # SQL is coo
391 $dbh->do("INSERT INTO permissions ($permlist,".($type eq 'user' ? 'user' : 'group')."_id) ".
392 "SELECT $permlist,? FROM permissions WHERE permission_id=?", undef, ($id,$permid) );
393 ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions ".
394 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($id) );
395 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='f',permission_id=? ".
396 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($permid, $id) );
397 }
398
399 # and now set the permissions we were passed
400 foreach (@permtypes) {
401 if (defined ($newperms->{$_})) {
402 $dbh->do("UPDATE permissions SET $_=? WHERE permission_id=?", undef, ($newperms->{$_},$permid) );
403 }
404 }
405
406 } # (inherited->)? custom
407
408 $dbh->commit;
409 }; # end eval
410 if ($@) {
411 my $msg = $@;
412 eval { $dbh->rollback; };
413 return ('FAIL',"$failmsg: $msg ($permid)");
414 } else {
415 return ('OK',$permid);
416 }
417
418} # end changePermissions()
419
420
421## DNSDB::comparePermissions()
422# Compare two permission hashes
423# Returns '>', '<', '=', '!'
424sub comparePermissions {
425 my $p1 = shift;
426 my $p2 = shift;
427
428 my $retval = '='; # assume equality until proven otherwise
429
430 no warnings "uninitialized";
431
432 foreach (@permtypes) {
433 next if $p1->{$_} == $p2->{$_}; # equal is good
434 if ($p1->{$_} && !$p2->{$_}) {
435 if ($retval eq '<') { # if we've already found an unequal pair where
436 $retval = '!'; # $p2 has more access, and we now find a pair
437 last; # where $p1 has more access, the overall access
438 } # is neither greater or lesser, it's unequal.
439 $retval = '>';
440 }
441 if (!$p1->{$_} && $p2->{$_}) {
442 if ($retval eq '>') { # if we've already found an unequal pair where
443 $retval = '!'; # $p1 has more access, and we now find a pair
444 last; # where $p2 has more access, the overall access
445 } # is neither greater or lesser, it's unequal.
446 $retval = '<';
447 }
448 }
449 return $retval;
450} # end comparePermissions()
451
452
453## DNSDB::changeGroup()
454# Change group ID of an entity
455# Takes a database handle, entity type, entity ID, and new group ID
456sub changeGroup {
457 my $dbh = shift;
458 my $type = shift;
459 my $id = shift;
460 my $newgrp = shift;
461
462##fixme: fail on not enough args
463 #return ('FAIL', "Missing
464
465 if ($type eq 'domain') {
466 $dbh->do("UPDATE domains SET group_id=? WHERE domain_id=?", undef, ($newgrp, $id))
467 or return ('FAIL','Group change failed: '.$dbh->errstr);
468 } elsif ($type eq 'user') {
469 $dbh->do("UPDATE users SET group_id=? WHERE user_id=?", undef, ($newgrp, $id))
470 or return ('FAIL','Group change failed: '.$dbh->errstr);
471 } elsif ($type eq 'group') {
472 $dbh->do("UPDATE groups SET parent_group_id=? WHERE group_id=?", undef, ($newgrp, $id))
473 or return ('FAIL','Group change failed: '.$dbh->errstr);
474 }
475 return ('OK','OK');
476} # end changeGroup()
477
478
479## DNSDB::_log()
480# Log an action
481# Internal sub
482# Takes a database handle, <foo>, <bar>
483sub _log {
484} # end _log
485
486
487##
488## Processing subs
489##
490
491## DNSDB::addDomain()
492# Add a domain
493# Takes a database handle, domain name, numeric group, and boolean(ish) state (active/inactive)
494# Returns a status code and message
495sub addDomain {
496 $errstr = '';
497 my $dbh = shift;
498 return ('FAIL',"Need database handle") if !$dbh;
499 my $domain = shift;
500 return ('FAIL',"Domain must not be blank") if !$domain;
501 my $group = shift;
502 return ('FAIL',"Need group") if !defined($group);
503 my $state = shift;
504 return ('FAIL',"Need domain status") if !defined($state);
505
506 $state = 1 if $state =~ /^active$/;
507 $state = 1 if $state =~ /^on$/;
508 $state = 0 if $state =~ /^inactive$/;
509 $state = 0 if $state =~ /^off$/;
510
511 return ('FAIL',"Invalid domain status") if $state !~ /^\d+$/;
512
513 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");
514 my $dom_id;
515
516# quick check to start to see if we've already got one
517 $sth->execute($domain);
518 ($dom_id) = $sth->fetchrow_array;
519
520 return ('FAIL', "Domain already exists") if $dom_id;
521
522 # Allow transactions, and raise an exception on errors so we can catch it later.
523 # Use local to make sure these get "reset" properly on exiting this block
524 local $dbh->{AutoCommit} = 0;
525 local $dbh->{RaiseError} = 1;
526
527 # Wrap all the SQL in a transaction
528 eval {
529 # insert the domain...
530 my $sth = $dbh->prepare("insert into domains (domain,group_id,status) values (?,?,?)");
531 $sth->execute($domain,$group,$state);
532
533 # get the ID...
534 $sth = $dbh->prepare("select domain_id from domains where domain='$domain'");
535 $sth->execute;
536 ($dom_id) = $sth->fetchrow_array();
537
538 # ... and now we construct the standard records from the default set. NB: group should be variable.
539 $sth = $dbh->prepare("select host,type,val,distance,weight,port,ttl from default_records where group_id=$group");
540 my $sth_in = $dbh->prepare("insert into records (domain_id,host,type,val,distance,weight,port,ttl)".
541 " values ($dom_id,?,?,?,?,?,?,?)");
542 $sth->execute;
543 while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $sth->fetchrow_array()) {
544 $host =~ s/DOMAIN/$domain/g;
545 $val =~ s/DOMAIN/$domain/g;
546 $sth_in->execute($host,$type,$val,$dist,$weight,$port,$ttl);
547 }
548
549 # once we get here, we should have suceeded.
550 $dbh->commit;
551 }; # end eval
552
553 if ($@) {
554 my $msg = $@;
555 eval { $dbh->rollback; };
556 return ('FAIL',$msg);
557 } else {
558 return ('OK',$dom_id);
559 }
560} # end addDomain
561
562
563## DNSDB::delDomain()
564# Delete a domain.
565# for now, just delete the records, then the domain.
566# later we may want to archive it in some way instead (status code 2, for example?)
567sub delDomain {
568 my $dbh = shift;
569 my $domid = shift;
570
571 # Allow transactions, and raise an exception on errors so we can catch it later.
572 # Use local to make sure these get "reset" properly on exiting this block
573 local $dbh->{AutoCommit} = 0;
574 local $dbh->{RaiseError} = 1;
575
576 my $failmsg = '';
577
578 # Wrap all the SQL in a transaction
579 eval {
580 my $sth = $dbh->prepare("delete from records where domain_id=?");
581 $failmsg = "Failure removing domain records";
582 $sth->execute($domid);
583 $sth = $dbh->prepare("delete from domains where domain_id=?");
584 $failmsg = "Failure removing domain";
585 $sth->execute($domid);
586
587 # once we get here, we should have suceeded.
588 $dbh->commit;
589 }; # end eval
590
591 if ($@) {
592 my $msg = $@;
593 eval { $dbh->rollback; };
594 return ('FAIL',"$failmsg: $msg");
595 } else {
596 return ('OK','OK');
597 }
598
599} # end delDomain()
600
601
602## DNSDB::domainName()
603# Return the domain name based on a domain ID
604# Takes a database handle and the domain ID
605# Returns the domain name or undef on failure
606sub domainName {
607 $errstr = '';
608 my $dbh = shift;
609 my $domid = shift;
610 my ($domname) = $dbh->selectrow_array("SELECT domain FROM domains WHERE domain_id=?", undef, ($domid) );
611 $errstr = $DBI::errstr if !$domname;
612 return $domname if $domname;
613} # end domainName()
614
615
616## DNSDB::domainID()
617# Takes a database handle and domain name
618# Returns the domain ID number
619sub domainID {
620 $errstr = '';
621 my $dbh = shift;
622 my $domain = shift;
623 my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain) );
624 $errstr = $DBI::errstr if !$domid;
625 return $domid if $domid;
626} # end domainID()
627
628
629## DNSDB::addGroup()
630# Add a group
631# Takes a database handle, group name, parent group, hashref for permissions,
632# and optional template-vs-cloneme flag
633# Returns a status code and message
634sub addGroup {
635 $errstr = '';
636 my $dbh = shift;
637 my $groupname = shift;
638 my $pargroup = shift;
639 my $permissions = shift;
640
641 # 0 indicates "custom", hardcoded.
642 # Any other value clones that group's default records, if it exists.
643 my $inherit = shift || 0;
644##fixme: need a flag to indicate clone records or <?> ?
645
646 # Allow transactions, and raise an exception on errors so we can catch it later.
647 # Use local to make sure these get "reset" properly on exiting this block
648 local $dbh->{AutoCommit} = 0;
649 local $dbh->{RaiseError} = 1;
650
651 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?");
652 my $group_id;
653
654# quick check to start to see if we've already got one
655 $sth->execute($groupname);
656 ($group_id) = $sth->fetchrow_array;
657
658 return ('FAIL', "Group already exists") if $group_id;
659
660 # Wrap all the SQL in a transaction
661 eval {
662 $sth = $dbh->prepare("INSERT INTO groups (parent_group_id,group_name) VALUES (?,?)");
663 $sth->execute($pargroup,$groupname);
664
665 $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?");
666 $sth->execute($groupname);
667 my ($groupid) = $sth->fetchrow_array();
668
669# Permissions
670 if ($inherit) {
671 } else {
672 my @permvals;
673 foreach (@permtypes) {
674 if (!defined ($permissions->{$_})) {
675 push @permvals, 0;
676 } else {
677 push @permvals, $permissions->{$_};
678 }
679 }
680
681 $sth = $dbh->prepare("INSERT INTO permissions (group_id,$permlist) values (?".',?'x($#permtypes+1).")");
682 $sth->execute($groupid,@permvals);
683
684 $sth = $dbh->prepare("SELECT permission_id FROM permissions WHERE group_id=?");
685 $sth->execute($groupid);
686 my ($permid) = $sth->fetchrow_array();
687
688 $dbh->do("UPDATE groups SET permission_id=$permid WHERE group_id=$groupid");
689 } # done permission fiddling
690
691# Default records
692 $sth = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ".
693 "VALUES ($groupid,?,?,?,?,?,?,?)");
694 if ($inherit) {
695 # Duplicate records from parent. Actually relying on inherited records feels
696 # very fragile, and it would be problematic to roll over at a later time.
697 my $sth2 = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?");
698 $sth2->execute($pargroup);
699 while (my @clonedata = $sth2->fetchrow_array) {
700 $sth->execute(@clonedata);
701 }
702 } else {
703##fixme: Hardcoding is Bad, mmmmkaaaay?
704 # reasonable basic defaults for SOA, MX, NS, and minimal hosting
705 # could load from a config file, but somewhere along the line we need hardcoded bits.
706 $sth->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400);
707 $sth->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200);
708 $sth->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200);
709 $sth->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200);
710 $sth->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200);
711 $sth->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200);
712 }
713
714 # once we get here, we should have suceeded.
715 $dbh->commit;
716 }; # end eval
717
718 if ($@) {
719 my $msg = $@;
720 eval { $dbh->rollback; };
721 return ('FAIL',$msg);
722 } else {
723 return ('OK','OK');
724 }
725
726} # end addGroup()
727
728
729## DNSDB::delGroup()
730# Delete a group.
731# Takes a group ID
732# Returns a status code and message
733sub delGroup {
734 my $dbh = shift;
735 my $groupid = shift;
736
737 # Allow transactions, and raise an exception on errors so we can catch it later.
738 # Use local to make sure these get "reset" properly on exiting this block
739 local $dbh->{AutoCommit} = 0;
740 local $dbh->{RaiseError} = 1;
741
742##fixme: locate "knowable" error conditions and deal with them before the eval
743# ... or inside, whatever.
744# -> domains still exist in group
745# -> ...
746 my $failmsg = '';
747
748 # Wrap all the SQL in a transaction
749 eval {
750 my $sth = $dbh->prepare("SELECT count(*) FROM domains WHERE group_id=?");
751 $sth->execute($groupid);
752 my ($domcnt) = $sth->fetchrow_array;
753 $failmsg = "Can't remove group ".groupName($dbh,$groupid);
754 die "$domcnt domains still in group\n" if $domcnt;
755
756 $sth = $dbh->prepare("delete from default_records where group_id=?");
757 $failmsg = "Failed to delete default records for ".groupName($dbh,$groupid);
758 $sth->execute($groupid);
759 $sth = $dbh->prepare("delete from groups where group_id=?");
760 $failmsg = "Failed to remove group ".groupName($dbh,$groupid);
761 $sth->execute($groupid);
762
763 # once we get here, we should have suceeded.
764 $dbh->commit;
765 }; # end eval
766
767 if ($@) {
768 my $msg = $@;
769 eval { $dbh->rollback; };
770 return ('FAIL',"$failmsg: $msg");
771 } else {
772 return ('OK','OK');
773 }
774} # end delGroup()
775
776
777## DNSDB::getChildren()
778# Get a list of all groups whose parent^n is group <n>
779# Takes a database handle, group ID, reference to an array to put the group IDs in,
780# and an optional flag to return only immediate children or all children-of-children
781# default to returning all children
782# Calls itself
783sub getChildren {
784 $errstr = '';
785 my $dbh = shift;
786 my $rootgroup = shift;
787 my $groupdest = shift;
788 my $immed = shift || 'all';
789
790 # special break for default group; otherwise we get stuck.
791 if ($rootgroup == 1) {
792 # by definition, group 1 is the Root Of All Groups
793 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE NOT (group_id=1)".
794 ($immed ne 'all' ? " AND parent_group_id=1" : ''));
795 $sth->execute;
796 while (my @this = $sth->fetchrow_array) {
797 push @$groupdest, @this;
798 }
799 } else {
800 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE parent_group_id=?");
801 $sth->execute($rootgroup);
802 return if $sth->rows == 0;
803 my @grouplist;
804 while (my ($group) = $sth->fetchrow_array) {
805 push @$groupdest, $group;
806 getChildren($dbh,$group,$groupdest) if $immed eq 'all';
807 }
808 }
809} # end getChildren()
810
811
812## DNSDB::groupName()
813# Return the group name based on a group ID
814# Takes a database handle and the group ID
815# Returns the group name or undef on failure
816sub groupName {
817 $errstr = '';
818 my $dbh = shift;
819 my $groupid = shift;
820 my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?");
821 $sth->execute($groupid);
822 my ($groupname) = $sth->fetchrow_array();
823 $errstr = $DBI::errstr if !$groupname;
824 return $groupname if $groupname;
825} # end groupName
826
827
828## DNSDB::groupID()
829# Return the group ID based on the group name
830# Takes a database handle and the group name
831# Returns the group ID or undef on failure
832sub groupID {
833 $errstr = '';
834 my $dbh = shift;
835 my $group = shift;
836 my ($grpid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group=?", undef, ($group) );
837 $errstr = $DBI::errstr if !$grpid;
838 return $grpid if $grpid;
839} # end groupID()
840
841
842## DNSDB::addUser()
843# Add a user.
844# Takes a DB handle, username, group ID, password, state (active/inactive).
845# Optionally accepts:
846# user type (user/admin) - defaults to user
847# permissions string - defaults to inherit from group
848# three valid forms:
849# i - Inherit permissions
850# c:<user_id> - Clone permissions from <user_id>
851# C:<permission list> - Set these specific permissions
852# first name - defaults to username
853# last name - defaults to blank
854# phone - defaults to blank (could put other data within column def)
855# Returns (OK,<uid>) on success, (FAIL,<message>) on failure
856sub addUser {
857 $errstr = '';
858 my $dbh = shift;
859 my $username = shift;
860 my $group = shift;
861 my $pass = shift;
862 my $state = shift;
863
864 return ('FAIL', "Missing one or more required entries") if !defined($state);
865 return ('FAIL', "Username must not be blank") if !$username;
866
867 my $type = shift || 'u'; # create limited users by default - fwiw, not sure yet how this will interact with ACLs
868
869 my $permstring = shift || 'i'; # default is to inhert permissions from group
870
871 my $fname = shift || $username;
872 my $lname = shift || '';
873 my $phone = shift || ''; # not going format-check
874
875 my $sth = $dbh->prepare("SELECT user_id FROM users WHERE username=?");
876 my $user_id;
877
878# quick check to start to see if we've already got one
879 $sth->execute($username);
880 ($user_id) = $sth->fetchrow_array;
881
882 return ('FAIL', "User already exists") if $user_id;
883
884 # Allow transactions, and raise an exception on errors so we can catch it later.
885 # Use local to make sure these get "reset" properly on exiting this block
886 local $dbh->{AutoCommit} = 0;
887 local $dbh->{RaiseError} = 1;
888
889 my $failmsg = '';
890
891 # Wrap all the SQL in a transaction
892 eval {
893 # insert the user... note we set inherited perms by default since
894 # it's simple and cleans up some other bits of state
895 my $sth = $dbh->prepare("INSERT INTO users ".
896 "(group_id,username,password,firstname,lastname,phone,type,status,permission_id,inherit_perm) ".
897 "VALUES (?,?,?,?,?,?,?,?,(SELECT permission_id FROM permissions WHERE group_id=?),'t')");
898 $sth->execute($group,$username,unix_md5_crypt($pass),$fname,$lname,$phone,$type,$state,$group);
899
900 # get the ID...
901 ($user_id) = $dbh->selectrow_array("SELECT currval('users_user_id_seq')");
902
903# Permissions! Gotta set'em all!
904 die "Invalid permission string $permstring"
905 if $permstring !~ /^(?:
906 i # inherit
907 |c:\d+ # clone
908 # custom. no, the leading , is not a typo
909 |C:(?:,(?:group|user|domain|record|self)_(?:edit|create|delete))*
910 )$/x;
911# bleh. I'd call another function to do my dirty work, but we're in the middle of a transaction already.
912 if ($permstring ne 'i') {
913 # for cloned or custom permissions, we have to create a new permissions entry.
914 my $clonesrc = $group;
915 if ($permstring =~ /^c:(\d+)/) { $clonesrc = $1; }
916 $dbh->do("INSERT INTO permissions ($permlist,user_id) ".
917 "SELECT $permlist,? FROM permissions WHERE permission_id=".
918 "(SELECT permission_id FROM permissions WHERE ".($permstring =~ /^c:/ ? 'user' : 'group')."_id=?)",
919 undef, ($user_id,$clonesrc) );
920 $dbh->do("UPDATE users SET permission_id=".
921 "(SELECT permission_id FROM permissions WHERE user_id=?) ".
922 "WHERE user_id=?", undef, ($user_id, $user_id) );
923 }
924 if ($permstring =~ /^C:/) {
925 # finally for custom permissions, we set the passed-in permissions (and unset
926 # any that might have been brought in by the clone operation above)
927 my ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions WHERE user_id=?",
928 undef, ($user_id) );
929 foreach (@permtypes) {
930 if ($permstring =~ /,$_/) {
931 $dbh->do("UPDATE permissions SET $_='t' WHERE permission_id=?", undef, ($permid) );
932 } else {
933 $dbh->do("UPDATE permissions SET $_='f' WHERE permission_id=?", undef, ($permid) );
934 }
935 }
936 }
937
938 $dbh->do("UPDATE users SET inherit_perm='n' WHERE user_id=?", undef, ($user_id) );
939
940##fixme: add another table to hold name/email for log table?
941
942 # once we get here, we should have suceeded.
943 $dbh->commit;
944 }; # end eval
945
946 if ($@) {
947 my $msg = $@;
948 eval { $dbh->rollback; };
949 return ('FAIL',$msg." $failmsg");
950 } else {
951 return ('OK',$user_id);
952 }
953} # end addUser
954
955
956## DNSDB::checkUser()
957# Check user/pass combo on login
958sub checkUser {
959 my $dbh = shift;
960 my $user = shift;
961 my $inpass = shift;
962
963 my $sth = $dbh->prepare("SELECT user_id,group_id,password,firstname,lastname FROM users WHERE username=?");
964 $sth->execute($user);
965 my ($uid,$gid,$pass,$fname,$lname) = $sth->fetchrow_array;
966 my $loginfailed = 1 if !defined($uid);
967
968 if ($pass =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) {
969 $loginfailed = 1 if $pass ne unix_md5_crypt($inpass,$1);
970 } else {
971 $loginfailed = 1 if $pass ne $inpass;
972 }
973
974 # nnnngggg
975 return ($uid, $gid);
976} # end checkUser
977
978
979## DNSDB:: updateUser()
980# Update general data about user
981sub updateUser {
982 my $dbh = shift;
983
984##fixme: tweak calling convention so that we can update any given bit of data
985 my $uid = shift;
986 my $username = shift;
987 my $group = shift;
988 my $pass = shift;
989 my $state = shift;
990 my $type = shift || 'u';
991 my $fname = shift || $username;
992 my $lname = shift || '';
993 my $phone = shift || ''; # not going format-check
994
995 my $failmsg = '';
996
997 # Allow transactions, and raise an exception on errors so we can catch it later.
998 # Use local to make sure these get "reset" properly on exiting this block
999 local $dbh->{AutoCommit} = 0;
1000 local $dbh->{RaiseError} = 1;
1001
1002 my $sth;
1003
1004 # Password can be left blank; if so we assume there's one on file.
1005 # Actual blank passwords are bad, mm'kay?
1006 if (!$pass) {
1007 $sth = $dbh->prepare("SELECT password FROM users WHERE user_id=?");
1008 $sth->execute($uid);
1009 ($pass) = $sth->fetchrow_array;
1010 } else {
1011 $pass = unix_md5_crypt($pass);
1012 }
1013
1014 eval {
1015 my $sth = $dbh->prepare(q(
1016 UPDATE users
1017 SET username=?, password=?, firstname=?, lastname=?, phone=?, type=?, status=?
1018 WHERE user_id=?
1019 )
1020 );
1021 $sth->execute($username, $pass, $fname, $lname, $phone, $type, $state, $uid);
1022 $dbh->commit;
1023 };
1024 if ($@) {
1025 my $msg = $@;
1026 eval { $dbh->rollback; };
1027 return ('FAIL',"$failmsg: $msg");
1028 } else {
1029 return ('OK','OK');
1030 }
1031} # end updateUser()
1032
1033
1034## DNSDB::delUser()
1035#
1036sub delUser {
1037 my $dbh = shift;
1038 return ('FAIL',"Need database handle") if !$dbh;
1039 my $userid = shift;
1040 return ('FAIL',"Missing userid") if !defined($userid);
1041
1042 my $sth = $dbh->prepare("delete from users where user_id=?");
1043 $sth->execute($userid);
1044
1045 return ('FAIL',"Couldn't remove user: ".$sth->errstr) if $sth->err;
1046
1047 return ('OK','OK');
1048
1049} # end delUser
1050
1051
1052## DNSDB::userFullName()
1053# Return a pretty string!
1054# Takes a user_id and optional printf-ish string to indicate which pieces where:
1055# %u for the username
1056# %f for the first name
1057# %l for the last name
1058# All other text in the passed string will be left as-is.
1059##fixme: need a "smart" option too, so that missing/null/blank first/last names don't give funky output
1060sub userFullName {
1061 $errstr = '';
1062 my $dbh = shift;
1063 my $userid = shift;
1064 my $fullformat = shift || '%f %l (%u)';
1065 my $sth = $dbh->prepare("select username,firstname,lastname from users where user_id=?");
1066 $sth->execute($userid);
1067 my ($uname,$fname,$lname) = $sth->fetchrow_array();
1068 $errstr = $DBI::errstr if !$uname;
1069
1070 $fullformat =~ s/\%u/$uname/g;
1071 $fullformat =~ s/\%f/$fname/g;
1072 $fullformat =~ s/\%l/$lname/g;
1073
1074 return $fullformat;
1075} # end userFullName
1076
1077
1078## DNSDB::userStatus()
1079# Sets and/or returns a user's status
1080# Takes a database handle, user ID and optionally a status argument
1081# Returns undef on errors.
1082sub userStatus {
1083 my $dbh = shift;
1084 my $id = shift;
1085 my $newstatus = shift;
1086
1087 return undef if $id !~ /^\d+$/;
1088
1089 my $sth;
1090
1091# ooo, fun! let's see what we were passed for status
1092 if ($newstatus) {
1093 $sth = $dbh->prepare("update users set status=? where user_id=?");
1094 # ass-u-me caller knows what's going on in full
1095 if ($newstatus =~ /^[01]$/) { # only two valid for now.
1096 $sth->execute($newstatus,$id);
1097 } elsif ($newstatus =~ /^usero(?:n|ff)$/) {
1098 $sth->execute(($newstatus eq 'useron' ? 1 : 0),$id);
1099 }
1100 }
1101
1102 $sth = $dbh->prepare("select status from users where user_id=?");
1103 $sth->execute($id);
1104 my ($status) = $sth->fetchrow_array;
1105 return $status;
1106} # end userStatus()
1107
1108
1109## DNSDB::getUserData()
1110# Get misc user data for display
1111sub getUserData {
1112 my $dbh = shift;
1113 my $uid = shift;
1114
1115 my $sth = $dbh->prepare("SELECT group_id,username,firstname,lastname,phone,type,status,inherit_perm ".
1116 "FROM users WHERE user_id=?");
1117 $sth->execute($uid);
1118 return $sth->fetchrow_hashref();
1119
1120} # end getUserData()
1121
1122
1123## DNSDB::getSOA()
1124# Return all suitable fields from an SOA record in separate elements of a hash
1125# Takes a database handle, default/live flag, and group (default) or domain (live) ID
1126sub getSOA {
1127 $errstr = '';
1128 my $dbh = shift;
1129 my $def = shift;
1130 my $id = shift;
1131 my %ret;
1132
1133 # (ab)use distance and weight columns to store SOA data
1134
1135 my $sql = "SELECT record_id,host,val,ttl,distance from";
1136 if ($def eq 'def' or $def eq 'y') {
1137 $sql .= " default_records WHERE group_id=? AND type=$reverse_typemap{SOA}";
1138 } else {
1139 # we're editing a live SOA record; find based on domain
1140 $sql .= " records WHERE domain_id=? AND type=$reverse_typemap{SOA}";
1141 }
1142 my $sth = $dbh->prepare($sql);
1143 $sth->execute($id);
1144
1145 my ($recid,$host,$val,$ttl,$serial) = $sth->fetchrow_array() or return;
1146 my ($prins,$contact) = split /:/, $host;
1147 my ($refresh,$retry,$expire,$minttl) = split /:/, $val;
1148
1149 $ret{recid} = $recid;
1150 $ret{ttl} = $ttl;
1151 $ret{serial} = $serial;
1152 $ret{prins} = $prins;
1153 $ret{contact} = $contact;
1154 $ret{refresh} = $refresh;
1155 $ret{retry} = $retry;
1156 $ret{expire} = $expire;
1157 $ret{minttl} = $minttl;
1158
1159 return %ret;
1160} # end getSOA()
1161
1162
1163## DNSDB::getRecLine()
1164# Return all data fields for a zone record in separate elements of a hash
1165# Takes a database handle, default/live flag, and record ID
1166sub getRecLine {
1167 $errstr = '';
1168 my $dbh = shift;
1169 my $def = shift;
1170 my $id = shift;
1171
1172 my $sql = "SELECT record_id,host,type,val,distance,weight,port,ttl".
1173 (($def eq 'def' or $def eq 'y') ? ',group_id FROM default_' : ',domain_id FROM ').
1174 "records WHERE record_id=?";
1175 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
1176
1177 if ($dbh->err) {
1178 $errstr = $DBI::errstr;
1179 return undef;
1180 }
1181
1182 if (!$ret) {
1183 $errstr = "No such record";
1184 return undef;
1185 }
1186
1187 $ret->{parid} = (($def eq 'def' or $def eq 'y') ? $ret->{group_id} : $ret->{domain_id});
1188
1189 return $ret;
1190}
1191
1192
1193##fixme: should use above (getRecLine()) to get lines for below?
1194## DNSDB::getDomRecs()
1195# Return records for a domain
1196# Takes a database handle, default/live flag, group/domain ID, start,
1197# number of records, sort field, and sort order
1198# Returns a reference to an array of hashes
1199sub getDomRecs {
1200 $errstr = '';
1201 my $dbh = shift;
1202 my $type = shift;
1203 my $id = shift;
1204 my $nrecs = shift || 'all';
1205 my $nstart = shift || 0;
1206
1207## for order, need to map input to column names
1208 my $order = shift || 'host';
1209 my $direction = shift || 'ASC';
1210
1211 $type = 'y' if $type eq 'def';
1212
1213 my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.distance,r.weight,r.port,r.ttl FROM ";
1214 $sql .= "default_" if $type eq 'y';
1215 $sql .= "records r ";
1216 $sql .= "INNER JOIN rectypes t ON r.type=t.val "; # for sorting by type alphabetically
1217 if ($type eq 'y') {
1218 $sql .= "WHERE r.group_id=?";
1219 } else {
1220 $sql .= "WHERE r.domain_id=?";
1221 }
1222 $sql .= " AND NOT r.type=$reverse_typemap{SOA}";
1223 # use alphaorder column for "correct" ordering of sort-by-type instead of DNS RR type number
1224 $sql .= " ORDER BY ".($order eq 'type' ? 't.alphaorder' : "r.$order")." $direction";
1225 $sql .= " LIMIT $nrecs OFFSET ".($nstart*$nrecs) if $nstart ne 'all';
1226
1227 my $sth = $dbh->prepare($sql) or warn $dbh->errstr;
1228 $sth->execute($id) or warn "$sql: ".$sth->errstr;
1229
1230 my @retbase;
1231 while (my $ref = $sth->fetchrow_hashref()) {
1232 push @retbase, $ref;
1233 }
1234
1235 my $ret = \@retbase;
1236 return $ret;
1237} # end getDomRecs()
1238
1239
1240## DNSDB::getRecCount()
1241# Return count of non-SOA records in domain (or default records in a group)
1242# Takes a database handle, default/live flag and group/domain ID
1243# Returns the count
1244sub getRecCount {
1245 my $dbh = shift;
1246 my $defrec = shift;
1247 my $id = shift;
1248
1249 my ($count) = $dbh->selectrow_array("SELECT count(*) FROM ".
1250 ($defrec eq 'y' ? 'default_' : '')."records ".
1251 "WHERE ".($defrec eq 'y' ? 'group' : 'domain')."_id=? ".
1252 "AND NOT type=$reverse_typemap{SOA}", undef, ($id) );
1253
1254 return $count;
1255
1256} # end getRecCount()
1257
1258
1259## DNSDB::addRec()
1260# Add a new record to a domain or a group's default records
1261# Takes a database handle, default/live flag, group/domain ID,
1262# host, type, value, and TTL
1263# Some types require additional detail: "distance" for MX and SRV,
1264# and weight/port for SRV
1265# Returns a status code and detail message in case of error
1266sub addRec {
1267 $errstr = '';
1268 my $dbh = shift;
1269 my $defrec = shift;
1270 my $id = shift;
1271
1272 my $host = shift;
1273 my $rectype = shift;
1274 my $val = shift;
1275 my $ttl = shift;
1276
1277 # Validation
1278 if ($rectype == $reverse_typemap{A} or $rectype == $reverse_typemap{AAAA}) {
1279 my $tmpip = new NetAddr::IP $val or
1280 return ("FAIL", "Address must be a valid IP address");
1281 }
1282
1283 my $fields = ($defrec eq 'y' ? 'group_id' : 'domain_id').",host,type,val,ttl";
1284 my $vallen = "?,?,?,?,?";
1285 my @vallist = ($id,$host,$rectype,$val,$ttl);
1286
1287 my $dist;
1288 if ($rectype == $reverse_typemap{MX} or $rectype == $reverse_typemap{SRV}) {
1289 $dist = shift;
1290 return ('FAIL',"Need distance for $typemap{$rectype} record") if !defined($dist);
1291 $fields .= ",distance";
1292 $vallen .= ",?";
1293 push @vallist, $dist;
1294 }
1295 my $weight;
1296 my $port;
1297 if ($rectype == $reverse_typemap{SRV}) {
1298 # check for _service._protocol. NB: RFC2782 does not say "MUST"... nor "SHOULD"...
1299 # it just says (paraphrased) "... is prepended with _ to prevent DNS collisions"
1300 return ('FAIL',"SRV records must begin with _service._protocol")
1301 if $host !~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-z0-9-]+/;
1302 $weight = shift;
1303 $port = shift;
1304 return ('FAIL',"Need weight and port for SRV record") if !defined($weight) or !defined($port);
1305 $fields .= ",weight,port";
1306 $vallen .= ",?,?";
1307 push @vallist, ($weight,$port);
1308 }
1309
1310 # Allow transactions, and raise an exception on errors so we can catch it later.
1311 # Use local to make sure these get "reset" properly on exiting this block
1312 local $dbh->{AutoCommit} = 0;
1313 local $dbh->{RaiseError} = 1;
1314
1315 eval {
1316 $dbh->do("INSERT INTO ".($defrec eq 'y' ? 'default_' : '')."records ($fields) VALUES ($vallen)",
1317 undef, @vallist);
1318 $dbh->commit;
1319 };
1320 if ($@) {
1321 my $msg = $@;
1322 eval { $dbh->rollback; };
1323 return ('FAIL',$msg);
1324 }
1325
1326 return ('OK','OK');
1327
1328} # end addRec()
1329
1330
1331## DNSDB::updateRec()
1332# Update a record
1333sub updateRec {
1334 $errstr = '';
1335
1336 my $dbh = shift;
1337 my $defrec = shift;
1338 my $id = shift;
1339
1340# all records have these
1341 my $host = shift;
1342 my $type = shift;
1343 my $val = shift;
1344 my $ttl = shift;
1345
1346 return('FAIL',"Missing standard argument(s)") if !defined($ttl);
1347
1348# only MX and SRV will use these
1349 my $dist = 0;
1350 my $weight = 0;
1351 my $port = 0;
1352
1353 if ($type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) {
1354 $dist = shift;
1355 return ('FAIL',"MX or SRV requires distance") if !defined($dist);
1356 if ($type == $reverse_typemap{SRV}) {
1357 $weight = shift;
1358 return ('FAIL',"SRV requires weight") if !defined($weight);
1359 $port = shift;
1360 return ('FAIL',"SRV requires port") if !defined($port);
1361 }
1362 }
1363
1364 local $dbh->{AutoCommit} = 0;
1365 local $dbh->{RaiseError} = 1;
1366
1367 eval {
1368 $dbh->do("UPDATE ".($defrec eq 'y' ? 'default_' : '')."records ".
1369 "SET host=?,val=?,type=?,ttl=?,distance=?,weight=?,port=? ".
1370 "WHERE record_id=?", undef, ($host, $val, $type, $ttl, $dist, $weight, $port, $id) );
1371 $dbh->commit;
1372 };
1373 if ($@) {
1374 my $msg = $@;
1375 $dbh->rollback;
1376 return ('FAIL', $msg);
1377 }
1378
1379 return ('OK','OK');
1380} # end updateRec()
1381
1382
1383## DNSDB::delRec()
1384# Delete a record.
1385sub delRec {
1386 $errstr = '';
1387 my $dbh = shift;
1388 my $defrec = shift;
1389 my $id = shift;
1390
1391 my $sth = $dbh->prepare("DELETE FROM ".($defrec eq 'y' ? 'default_' : '')."records WHERE record_id=?");
1392 $sth->execute($id);
1393
1394 return ('FAIL',"Couldn't remove record: ".$sth->errstr) if $sth->err;
1395
1396 return ('OK','OK');
1397} # end delRec()
1398
1399
1400 # Reference hashes.
1401 my %par_tbl = (
1402 group => 'groups',
1403 user => 'users',
1404 defrec => 'default_records',
1405 domain => 'domains',
1406 record => 'records'
1407 );
1408 my %id_col = (
1409 group => 'group_id',
1410 user => 'user_id',
1411 defrec => 'record_id',
1412 domain => 'domain_id',
1413 record => 'record_id'
1414 );
1415 my %par_col = (
1416 group => 'parent_group_id',
1417 user => 'group_id',
1418 defrec => 'group_id',
1419 domain => 'group_id',
1420 record => 'domain_id'
1421 );
1422 my %par_type = (
1423 group => 'group',
1424 user => 'group',
1425 defrec => 'group',
1426 domain => 'group',
1427 record => 'domain'
1428 );
1429
1430## DNSDB::getParents()
1431# Find out which entities are parent to the requested id
1432# Returns arrayref containing hash pairs of id/type
1433sub getParents {
1434 my $dbh = shift;
1435 my $id = shift;
1436 my $type = shift;
1437 my $depth = shift || 'all'; # valid values: 'all', 'immed', <int> (stop at this group ID)
1438
1439 my @parlist;
1440
1441 while (1) {
1442 my $result = $dbh->selectrow_hashref("SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?",
1443 undef, ($id) );
1444 unshift @parlist, ($result->{$par_col{$type}} => $par_type{$type});
1445 last if $result->{$par_col{$type}} == 1; # group 1 is its own parent
1446 $type = $par_type{$type};
1447 $id = $result->{$par_col{$type}};
1448 }
1449
1450 return \@parlist;
1451
1452} # end getParents()
1453
1454
1455## DNSDB::isParent()
1456# Returns true if $id1 is a parent of $id2, false otherwise
1457sub isParent {
1458 my $dbh = shift;
1459 my $id1 = shift;
1460 my $type1 = shift;
1461 my $id2 = shift;
1462 my $type2 = shift;
1463##todo: immediate, secondary, full (default)
1464
1465 # Return false on impossible relations
1466 return 0 if $type1 eq 'record'; # nothing may be a child of a record
1467 return 0 if $type1 eq 'defrec'; # nothing may be a child of a record
1468 return 0 if $type1 eq 'user'; # nothing may be child of a user
1469 return 0 if $type1 eq 'domain' && $type2 ne 'record'; # domain may not be a parent of anything other than a record
1470
1471 # group 1 is the ultimate root parent
1472 return 1 if $type1 eq 'group' && $id1 == 1;
1473
1474# almost the same loop as getParents() above
1475 my $id = $id2;
1476 my $type = $type2;
1477 my $foundparent = 0;
1478my $tmp = 0;
1479 while (1) {
1480my $sql = "SELECT $par_col{$type} FROM $par_tbl{$type} WHERE $id_col{$type} = ?";
1481 my $result = $dbh->selectrow_hashref($sql,
1482 undef, ($id) ) or warn $dbh->errstr." $sql";
1483 if ($result->{$par_col{$type}} == $id1) {
1484 $foundparent = 1;
1485 last;
1486 }
1487 # group 1 is its own parent. need this here more to break strange loops than for detecting a parent
1488 last if $result->{$par_col{$type}} == 1;
1489 $type = $par_type{$type};
1490 $id = $result->{$par_col{$type}};
1491last if $tmp++ > 10;
1492 }
1493
1494 return $foundparent;
1495} # end isParent()
1496
1497
1498## DNSDB::domStatus()
1499# Sets and/or returns a domain's status
1500# Takes a database handle, domain ID and optionally a status argument
1501# Returns undef on errors.
1502sub domStatus {
1503 my $dbh = shift;
1504 my $id = shift;
1505 my $newstatus = shift;
1506
1507 return undef if $id !~ /^\d+$/;
1508
1509 my $sth;
1510
1511# ooo, fun! let's see what we were passed for status
1512 if ($newstatus) {
1513 $sth = $dbh->prepare("update domains set status=? where domain_id=?");
1514 # ass-u-me caller knows what's going on in full
1515 if ($newstatus =~ /^[01]$/) { # only two valid for now.
1516 $sth->execute($newstatus,$id);
1517 } elsif ($newstatus =~ /^domo(?:n|ff)$/) {
1518 $sth->execute(($newstatus eq 'domon' ? 1 : 0),$id);
1519 }
1520 }
1521
1522 $sth = $dbh->prepare("select status from domains where domain_id=?");
1523 $sth->execute($id);
1524 my ($status) = $sth->fetchrow_array;
1525 return $status;
1526} # end domStatus()
1527
1528
1529## DNSDB::importAXFR
1530# Import a domain via AXFR
1531# Takes AXFR host, domain to transfer, group to put the domain in,
1532# and optionally:
1533# - active/inactive state flag (defaults to active)
1534# - overwrite-SOA flag (defaults to off)
1535# - overwrite-NS flag (defaults to off, doesn't affect subdomain NS records)
1536# Returns a status code (OK, WARN, or FAIL) and message - message should be blank
1537# if status is OK, but WARN includes conditions that are not fatal but should
1538# really be reported.
1539sub importAXFR {
1540 my $dbh = shift;
1541 my $ifrom_in = shift;
1542 my $domain = shift;
1543 my $group = shift;
1544 my $status = shift || 1;
1545 my $rwsoa = shift || 0;
1546 my $rwns = shift || 0;
1547
1548##fixme: add mode to delete&replace, merge+overwrite, merge new?
1549
1550 my $nrecs = 0;
1551 my $soaflag = 0;
1552 my $nsflag = 0;
1553 my $warnmsg = '';
1554 my $ifrom;
1555
1556 # choke on possible bad setting in ifrom
1557 # IPv4 and v6, and valid hostnames!
1558 ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i);
1559 return ('FAIL', "Bad AXFR source host $ifrom")
1560 unless ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i);
1561
1562 # Allow transactions, and raise an exception on errors so we can catch it later.
1563 # Use local to make sure these get "reset" properly on exiting this block
1564 local $dbh->{AutoCommit} = 0;
1565 local $dbh->{RaiseError} = 1;
1566
1567 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");
1568 my $dom_id;
1569
1570# quick check to start to see if we've already got one
1571 $sth->execute($domain);
1572 ($dom_id) = $sth->fetchrow_array;
1573
1574 return ('FAIL', "Domain already exists") if $dom_id;
1575
1576 eval {
1577 # can't do this, can't nest transactions. sigh.
1578 #my ($dcode, $dmsg) = addDomain(dbh, domain, group, status);
1579
1580##fixme: serial
1581 my $sth = $dbh->prepare("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)");
1582 $sth->execute($domain,$group,$status);
1583
1584## bizarre DBI<->Net::DNS interaction bug:
1585## sometimes a zone will cause an immediate commit-and-exit (sort of) of the while()
1586## fixed, apparently I was doing *something* odd, but not certain what it was that
1587## caused a commit instead of barfing
1588
1589 # get domain id so we can do the records
1590 $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");
1591 $sth->execute($domain);
1592 ($dom_id) = $sth->fetchrow_array();
1593
1594 my $res = Net::DNS::Resolver->new;
1595 $res->nameservers($ifrom);
1596 $res->axfr_start($domain)
1597 or die "Couldn't begin AXFR\n";
1598
1599 while (my $rr = $res->axfr_next()) {
1600 my $type = $rr->type;
1601
1602 my $sql = "INSERT INTO records (domain_id,host,type,ttl,val";
1603 my $vallen = "?,?,?,?,?";
1604
1605 $soaflag = 1 if $type eq 'SOA';
1606 $nsflag = 1 if $type eq 'NS';
1607
1608 my @vallist = ($dom_id, $rr->name, $reverse_typemap{$type}, $rr->ttl);
1609
1610# "Primary" types:
1611# A, NS, CNAME, SOA, PTR(warn in forward), MX, TXT, AAAA, SRV, A6(ob), SPF
1612# maybe KEY
1613
1614# nasty big ugly case-like thing here, since we have to do *some* different
1615# processing depending on the record. le sigh.
1616
1617##fixme: what record types other than TXT can/will have >255-byte payloads?
1618
1619 if ($type eq 'A') {
1620 push @vallist, $rr->address;
1621 } elsif ($type eq 'NS') {
1622# hmm. should we warn here if subdomain NS'es are left alone?
1623 next if ($rwns && ($rr->name eq $domain));
1624 push @vallist, $rr->nsdname;
1625 $nsflag = 1;
1626 } elsif ($type eq 'CNAME') {
1627 push @vallist, $rr->cname;
1628 } elsif ($type eq 'SOA') {
1629 next if $rwsoa;
1630 $vallist[1] = $rr->mname.":".$rr->rname;
1631 push @vallist, ($rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum);
1632 $soaflag = 1;
1633 } elsif ($type eq 'PTR') {
1634 push @vallist, $rr->ptrdname;
1635 # hmm. PTR records should not be in forward zones.
1636 } elsif ($type eq 'MX') {
1637 $sql .= ",distance";
1638 $vallen .= ",?";
1639 push @vallist, $rr->exchange;
1640 push @vallist, $rr->preference;
1641 } elsif ($type eq 'TXT') {
1642##fixme: Net::DNS docs say this should be deprecated for rdatastr() or char_str_list(),
1643## but don't really seem enthusiastic about it.
1644 my $rrdata = $rr->txtdata;
1645 push @vallist, $rrdata;
1646 } elsif ($type eq 'SPF') {
1647##fixme: and the same caveat here, since it is apparently a clone of ::TXT
1648 my $rrdata = $rr->txtdata;
1649 push @vallist, $rrdata;
1650 } elsif ($type eq 'AAAA') {
1651 push @vallist, $rr->address;
1652 } elsif ($type eq 'SRV') {
1653 $sql .= ",distance,weight,port" if $type eq 'SRV';
1654 $vallen .= ",?,?,?" if $type eq 'SRV';
1655 push @vallist, $rr->target;
1656 push @vallist, $rr->priority;
1657 push @vallist, $rr->weight;
1658 push @vallist, $rr->port;
1659 } elsif ($type eq 'KEY') {
1660 # we don't actually know what to do with these...
1661 push @vallist, ($rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname);
1662 } else {
1663 my $rrdata = $rr->rdatastr;
1664 push @vallist, $rrdata;
1665 # Finding a different record type is not fatal.... just problematic.
1666 # We may not be able to export it correctly.
1667 $warnmsg .= "Unusual record ".$rr->name." ($type) found\n";
1668 }
1669
1670# BIND supports:
1671# A CNAME HINFO MB(ex) MD(ob) MF(ob) MG(ex) MINFO(ex) MR(ex) MX NS NULL
1672# PTR SOA TXT WKS AFSDB(ex) ISDN(ex) RP(ex) RT(ex) X25(ex) PX
1673# ... if one can ever find the right magic to format them correctly
1674
1675# Net::DNS supports:
1676# RRSIG SIG NSAP NS NIMLOC NAPTR MX MR MINFO MG MB LOC ISDN IPSECKEY HINFO
1677# EID DNAME CNAME CERT APL AFSDB AAAA A DS NXT NSEC3PARAM NSEC3 NSEC KEY
1678# DNSKEY DLV X25 TXT TSIG TKEY SSHFP SRV SPF SOA RT RP PX PTR NULL APL::AplItem
1679
1680 $sth = $dbh->prepare($sql.") VALUES (".$vallen.")") or die "problem preparing record insert SQL\n";
1681 $sth->execute(@vallist) or die "failed to insert ".$rr->string.": ".$sth->errstr."\n";
1682
1683 $nrecs++;
1684
1685 } # while axfr_next
1686
1687 # Overwrite SOA record
1688 if ($rwsoa) {
1689 $soaflag = 1;
1690 my $sthgetsoa = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
1691 my $sthputsoa = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)");
1692 $sthgetsoa->execute($group,$reverse_typemap{SOA});
1693 while (my ($host,$val,$ttl) = $sthgetsoa->fetchrow_array()) {
1694 $host =~ s/DOMAIN/$domain/g;
1695 $val =~ s/DOMAIN/$domain/g;
1696 $sthputsoa->execute($dom_id,$host,$reverse_typemap{SOA},$val,$ttl);
1697 }
1698 }
1699
1700 # Overwrite NS records
1701 if ($rwns) {
1702 $nsflag = 1;
1703 my $sthgetns = $dbh->prepare("SELECT host,val,ttl FROM default_records WHERE group_id=? AND type=?");
1704 my $sthputns = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,ttl) VALUES (?,?,?,?,?)");
1705 $sthgetns->execute($group,$reverse_typemap{NS});
1706 while (my ($host,$val,$ttl) = $sthgetns->fetchrow_array()) {
1707 $host =~ s/DOMAIN/$domain/g;
1708 $val =~ s/DOMAIN/$domain/g;
1709 $sthputns->execute($dom_id,$host,$reverse_typemap{NS},$val,$ttl);
1710 }
1711 }
1712
1713 die "No records found; either $ifrom is not authoritative or doesn't allow transfers\n" if !$nrecs;
1714 die "Bad zone: No SOA record!\n" if !$soaflag;
1715 die "Bad zone: No NS records!\n" if !$nsflag;
1716
1717 $dbh->commit;
1718
1719 };
1720
1721 if ($@) {
1722 my $msg = $@;
1723 eval { $dbh->rollback; };
1724 return ('FAIL',$msg." $warnmsg");
1725 } else {
1726 return ('WARN', $warnmsg) if $warnmsg;
1727 return ('OK',"Imported OK");
1728 }
1729
1730 # it should be impossible to get here.
1731 return ('WARN',"OOOK!");
1732} # end importAXFR()
1733
1734
1735## DNSDB::export()
1736# Export the DNS database, or a part of it
1737# Takes database handle, export type, optional arguments depending on type
1738# Writes zone data to targets as appropriate for type
1739sub export {
1740 my $dbh = shift;
1741 my $target = shift;
1742
1743 if ($target eq 'tiny') {
1744 __export_tiny($dbh,@_);
1745 }
1746# elsif ($target eq 'foo') {
1747# __export_foo($dbh,@_);
1748#}
1749# etc
1750
1751} # end export()
1752
1753
1754## DNSDB::__export_tiny
1755# Internal sub to implement tinyDNS (compatible) export
1756# Takes database handle, filehandle to write export to, optional argument(s)
1757# to determine which data gets exported
1758sub __export_tiny {
1759 my $dbh = shift;
1760 my $datafile = shift;
1761
1762##fixme: slurp up further options to specify particular zone(s) to export
1763
1764 ## Convert a bare number into an octal-coded pair of octets.
1765 # Take optional arg to indicate a decimal or hex input. Defaults to hex.
1766 sub octalize {
1767 my $tmp = shift;
1768 my $srctype = shift || 'h'; # default assumes hex string
1769 $tmp = sprintf "%0.4x", hex($tmp) if $srctype eq 'h'; # 0-pad hex to 4 digits
1770 $tmp = sprintf "%0.4x", $tmp if $srctype eq 'd'; # 0-pad decimal to 4 hex digits
1771 my @o = ($tmp =~ /^(..)(..)$/); # split into octets
1772 return sprintf "\\%0.3o\\%0.3o", hex($o[0]), hex($o[1]);;
1773 }
1774
1775##fixme: fail if $datafile isn't an open, writable file
1776
1777 # easy case - export all evarything
1778 # not-so-easy case - export item(s) specified
1779 # todo: figure out what kind of list we use to export items
1780
1781 my $domsth = $dbh->prepare("SELECT domain_id,domain,status FROM domains WHERE status=1");
1782 my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl ".
1783 "FROM records WHERE domain_id=?");
1784 $domsth->execute();
1785 while (my ($domid,$dom,$domstat) = $domsth->fetchrow_array) {
1786 $recsth->execute($domid);
1787 while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $recsth->fetchrow_array) {
1788##fixme: need to store location in the db, and retrieve it here.
1789# temporarily hardcoded to empty so we can include it further down.
1790my $loc = '';
1791
1792##fixme: record validity timestamp. tinydns supports fiddling with timestamps.
1793# note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps.
1794# timestamps are TAI64
1795# ~~ 2^62 + time()
1796my $stamp = '';
1797
1798# raw packet in unknown format: first byte indicates length
1799# of remaining data, allows up to 255 raw bytes
1800
1801##fixme? append . to all host/val hostnames
1802 if ($typemap{$type} eq 'SOA') {
1803
1804 # host contains pri-ns:responsible
1805 # val is abused to contain refresh:retry:expire:minttl
1806##fixme: "manual" serial vs tinydns-autoserial
1807 print $datafile "Z$host"."::$val:$ttl:$stamp:$loc\n";
1808
1809 } elsif ($typemap{$type} eq 'A') {
1810
1811 print $datafile "+$host:$val:$ttl:$stamp:$loc\n";
1812
1813 } elsif ($typemap{$type} eq 'NS') {
1814
1815 print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n";
1816
1817 } elsif ($typemap{$type} eq 'AAAA') {
1818
1819 print $datafile ":$host:28:";
1820 my $altgrp = 0;
1821 my @altconv;
1822 # Split in to up to 8 groups of hex digits (allows for IPv6 :: 0-collapsing)
1823 foreach (split /:/, $val) {
1824 if (/^$/) {
1825 # flag blank entry; this is a series of 0's of (currently) unknown length
1826 $altconv[$altgrp++] = 's';
1827 } else {
1828 # call sub to convert 1-4 hex digits to 2 string-rep octal bytes
1829 $altconv[$altgrp++] = octalize($_)
1830 }
1831 }
1832 foreach my $octet (@altconv) {
1833 # if not 's', output
1834 print $datafile $octet unless $octet =~ /^s$/;
1835 # if 's', output (9-array length)x literal '\000\000'
1836 print $datafile '\000\000'x(9-$altgrp) if $octet =~ /^s$/;
1837 }
1838 print $datafile ":$ttl:$stamp:$loc\n";
1839
1840 } elsif ($typemap{$type} eq 'MX') {
1841
1842 print $datafile "\@$host"."::$val:$dist:$ttl:$stamp:$loc\n";
1843
1844 } elsif ($typemap{$type} eq 'TXT') {
1845
1846##fixme: split v-e-r-y long TXT strings? will need to do so for BIND export, at least
1847 $val =~ s/:/\\072/g; # may need to replace other symbols
1848 print $datafile "'$host:$val:$ttl:$stamp:$loc\n";
1849
1850# by-hand TXT
1851#:deepnet.cx:16:2v\075spf1\040a\040a\072bacon.deepnet.cx\040a\072home.deepnet.cx\040-all:3600
1852#@ IN TXT "v=spf1 a a:bacon.deepnet.cx a:home.deepnet.cx -all"
1853#'deepnet.cx:v=spf1 a a\072bacon.deepnet.cx a\072home.deepnet.cx -all:3600
1854
1855#txttest IN TXT "v=foo bar:bob kn;ob' \" !@#$%^&*()-=_+[]{}<>?"
1856#: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
1857
1858# very long TXT record as brought in by axfr-get
1859# note tinydns does not support >512-byte RR data, need axfr-dns (for TCP support) for that
1860# also note, tinydns does not seem to support <512, >256-byte RRdata from axfr-get either. :/
1861#:longtxt.deepnet.cx:16:
1862#\170this is a very long txt record. it is really long. long. very long. really very long. this is a very long txt record.
1863#\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.
1864#\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.
1865#:3600
1866
1867 } elsif ($typemap{$type} eq 'CNAME') {
1868
1869 print $datafile "C$host:$val:$ttl:$stamp:$loc\n";
1870
1871 } elsif ($typemap{$type} eq 'SRV') {
1872
1873 # data is two-byte values for priority, weight, port, in that order,
1874 # followed by length/string data
1875
1876 print $datafile ":$host:33:".octalize($dist,'d').octalize($weight,'d').octalize($port,'d');
1877
1878 $val .= '.' if $val !~ /\.$/;
1879 foreach (split /\./, $val) {
1880 printf $datafile "\\%0.3o%s", length($_), $_;
1881 }
1882 print $datafile "\\000:$ttl:$stamp:$loc\n";
1883
1884 } elsif ($typemap{$type} eq 'RP') {
1885
1886 # RP consists of two mostly free-form strings.
1887 # The first is supposed to be an email address with @ replaced by . (as with the SOA contact)
1888 # The second is the "hostname" of a TXT record with more info.
1889 print $datafile ":$host:17:";
1890 my ($who,$what) = split /\s/, $val;
1891 foreach (split /\./, $who) {
1892 printf $datafile "\\%0.3o%s", length($_), $_;
1893 }
1894 print $datafile '\000';
1895 foreach (split /\./, $what) {
1896 printf $datafile "\\%0.3o%s", length($_), $_;
1897 }
1898 print $datafile "\\000:$ttl:$stamp:$loc\n";
1899
1900 } elsif ($typemap{$type} eq 'PTR') {
1901
1902 # must handle both IPv4 and IPv6
1903##work
1904 # data should already be in suitable reverse order.
1905 print $datafile "^$host:$val:$ttl:$stamp:$loc\n";
1906
1907 } else {
1908 # raw record. we don't know what's in here, so we ASS-U-ME the user has
1909 # put it in correctly, since either the user is messing directly with the
1910 # database, or the record was imported via AXFR
1911 # <split by char>
1912 # convert anything not a-zA-Z0-9.- to octal coding
1913
1914##fixme: add flag to export "unknown" record types - note we'll probably end up
1915# mangling them since they were written to the DB from Net::DNS::RR::<type>->rdatastr.
1916 #print $datafile ":$host:$type:$val:$ttl:$stamp:$loc\n";
1917
1918 } # record type if-else
1919
1920 } # while ($recsth)
1921 } # while ($domsth)
1922} # end __export_tiny()
1923
1924
1925# shut Perl up
19261;
Note: See TracBrowser for help on using the repository browser.