source: trunk/tools/whoind/whoind @ 245

Revision 245, 13.5 KB checked in by sque, 8 months ago (diff)

Merge branches/awmn 221:235
[awmn] Allow users to delete zones and nameservers without making a request to hostmaster
[awmn] Untaint sprintf
[awmn] Don't change to php script dir
[awmn] Add support to dump list of stealth nameservers
[awmn] Add active peers search fields to hostmaster's ranges page
[awmn] Redirect errors to stderr
[awmn] Don't explicitly set owner to 'N' (#146)
[awmn] Restrict ip ranges to valid b-classes (0-255)
[awmn] Escape HTML special characters
[awmn] Use appropriate entities for < and > HTML special characters
[awmn] Add missing 'alt' attribute for 'img' element
[awmn] Close 'td' and 'tr' elements properly
[awmn] Escape HTML special characters in services URLs
[awmn] Remove end of line carriage returns

Line 
1#!/usr/bin/perl -T
2
3# Simple WHOIS server to serve WiND data
4# For more information have a look at http://wind.cube.gr/
5#
6# Copyright (c) 2005 Faidon Liambotis <faidon@cube.gr>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; version 2 dated June, 1991.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20
21use strict;
22use warnings;
23use Getopt::Long;
24use Pod::Usage;
25use DBI;
26use Socket qw(:DEFAULT :crlf);
27use POSIX qw(:sys_wait_h);
28use Sys::Syslog;
29use Fcntl;
30use Cwd;
31
32### Default command-line options
33
34# Process
35my $daemon = 0;
36my $user = 'nobody';
37my $group = 'nogroup';
38my $port = 43;
39
40# Location of the configuration file
41my $conf = '/etc/whoind.conf';
42my $pidfile = '/var/run/whoind/whoind.pid';
43
44### Version & Copyright info
45
46my $version = '0.9';
47my $name = "whoind $version\n";
48my $copyright = <<DONE ;
49Copyright (c) 2005 Faidon Liambotis <faidon\@cube.gr>
50Report bugs to http://wind.cube.gr/
51
52DONE
53
54### Let's do the job! ;-)
55
56# Configuration's variables
57my (%database, %zone, $header, $footer, $website);
58
59# Function prototypes
60sub usage();
61sub readconf($);
62sub daemonize();
63sub writepid($);
64sub drop_privileges();
65sub prepare();
66sub serve();
67sub cleanup();
68sub lookup($);
69sub msg($$);
70sub isIP($);
71sub ip2long($);
72sub long2ip($);
73sub formatipr($$);
74sub outpad($$);
75
76# Parse command-line options
77GetOptions( 'daemon|d!' => \$daemon,
78            'port|p=i' => \$port,
79            'user|u=s' => \$user,
80            'group|g=s' => \$group,
81            'config|conf|c=s' => \$conf,
82            'pidfile|pid|i=s' => \$pidfile,
83            'help|h|?' => \&usage )
84        or usage;
85
86# Handle HUP...
87$SIG{HUP} =
88        sub {
89                print msg('notice',
90                          "Received HUP signal, reloading configuration\n");
91        };
92# ... and INT/TERM
93$SIG{INT} = $SIG{TERM} = 
94        sub { 
95                die "Received $_[0] signal, terminating.\n";
96        };
97
98# syslog die() and warn() calls
99$SIG{__DIE__} =
100        sub {
101                my $err = "@_";
102                chomp($err);
103                msg('crit', $err);
104                cleanup;
105                die "$err\n";
106        };
107
108# Print program title/version and copyright information
109print $name.$copyright;
110
111# Foreground or background?
112unless ($daemon) {
113        print "Hit Ctrl+C to quit.\n";
114} else {
115        print "Forking to the background.\n";
116}
117
118# Use absolute path for the conf because we chdir '/' when we daemonize
119$conf = Cwd::abs_path($conf);
120
121# Syslog init
122openlog($0, 'cons,pid', 'daemon') if $daemon;
123msg('notice', "Starting $name");
124
125# Prepare socket; bind and listen
126prepare;
127# Security: drop privileges to $user/$group
128drop_privileges;
129# Daemonize if requested
130daemonize if $daemon;
131
132# Loop until SIGHUP breaks us
133while (1) {
134        # Read the configuration
135        readconf($conf);
136        # Serve incoming requests
137        serve;
138}
139
140# Should never reach here
141die "Abnormal termination\n";
142
143sub usage() {
144        pod2usage(-verbose => 1, -exitval => 2);
145}
146
147# Read and parse the configuration file
148sub readconf($) {
149        my $filename = shift;
150        my %opt;
151
152        # Cleanup previous values
153        undef %database; undef %zone;
154        undef $header; undef $footer; undef $website;
155       
156        # Open it
157        sysopen(FH, $filename, O_RDONLY)
158                or die "Error while opening $filename: $!\n";
159        # Filter it and put all options to %opt
160        while (<FH>) {
161                chomp;
162                # Remove comments...
163                s/(\s|^)+#.*//;
164                # ...leading whitespace...
165                s/^\s+//;
166                # ...and trailing whitespace
167                s/\s+$//;
168                next unless length;
169
170                # foo = bar becomes $opt{foo} = bar
171                my ($var, $value) = split(/\s*=\s*/, $_, 2);
172                $opt{$var} = $value;
173        }
174        close FH;
175
176        # Parse options to the respective variables
177        my ($key, $opt);
178        foreach $key (qw/host user password db charset/) {
179                $opt = $opt{"database.$key"};
180                defined $opt or
181                        die "Error while parsing config $filename\n";
182                $database{$key} = $opt;
183        }
184        foreach $key (qw/forward ns/) {
185                $opt = $opt{"zone.$key"};
186                defined $opt or
187                        die "Error while parsing config $filename\n";
188                $zone{$key} = $opt;
189        }
190        $header = $opt{header};
191        $footer = $opt{footer};
192        $website = $opt{website};
193}
194
195# Fork to the background properly
196sub daemonize() {
197        # "I'm a good daemon"
198        chdir '/'
199                or die "Error: Can't chdir to /, $!\n";
200        open STDIN, '/dev/null'
201                or die "Error: Can't read /dev/null, $!\n";
202        open STDOUT, '>/dev/null'
203                or die "Error: Can't write to /dev/null, $!\n";
204
205        # Fork to the background
206        defined(my $pid = fork)
207                or die "Error: Could not fork child, $!\n";
208        exit if $pid;
209       
210        POSIX::setsid
211                or die "Error: Can't start a new session, $!\n";
212       
213        writepid($pidfile);
214
215        # Redirect stderr too
216        open STDERR, '>&STDOUT'
217                or die "Error: Can't dup stdout, $!\n";
218}
219
220# Write PID file
221sub writepid($) {
222        my $filename = shift;
223        # Check if a pidfile exists and is writeable...
224        unless (sysopen(FH, $filename, O_WRONLY)) {
225                # ...else delete it and create a new one
226                unlink $filename;
227                sysopen(FH, $filename, O_WRONLY|O_CREAT)
228                        or die "Error while opening $filename: $!\n";
229        }
230        print FH "$$\n";
231        close FH;
232}
233
234sub drop_privileges() {
235        # Drop privileges, we don't need them any more
236        if ($group) {
237                my $gid = getgrnam($group)
238                        or die "Unable to get ID of $group: $!\n";
239                POSIX::setgid($gid)
240                        or die "Error: Could not drop group ID: $!\n";
241        }
242        if ($user) {
243                my $uid = getpwnam($user)
244                        or die "Unable to get ID of $user: $!\n";
245                POSIX::setuid($uid)
246                        or die "Error: Could not drop user ID: $!\n";
247        }
248}
249
250# Prepare SOCKET, bind & listen
251sub prepare() {
252        # Create the socket
253        socket(SERVER, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
254                or die "Error: Could not create socket: $!\n";
255
256        # Make it reusable     
257        setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1)
258                or die "Error: Could not set socket options: $!\n";
259
260        # Untaint port...
261        if ($port =~ m/^([0-9]+)$/) {
262                $port = $1;
263        } else {
264                die "Error: Invalid port number\n";
265        }
266        # ...and grab it
267        my $sockaddr = sockaddr_in($port, INADDR_ANY);
268       
269        # bind & listen
270        bind(SERVER, $sockaddr)
271                or die "Error: Could not bind to port $port: $!\n";
272        listen(SERVER, SOMAXCONN)
273                or die "Error: Could not listen on port $port: $!\n";
274}
275
276# Serve requests
277sub serve() {
278        # Avoid zombie processes
279        local $SIG{CHLD} = 'IGNORE';
280
281        # Forking server
282        while (accept(CLIENT, SERVER)) {
283                # Fork a child
284                next if my $pid = fork;
285
286                ### Child
287                # Close SERVER socket, useless to child
288                close(SERVER);
289
290                # "All requests are terminated with
291                # ASCII CR and then ASCII LF." --RFC 3912, WHOIS Protocol
292                local ($/) = $CRLF;
293               
294                # alarm: 10-second timeout for client input
295                local $SIG{ALRM} = sub { close(CLIENT); exit; };
296                alarm(10);
297
298                # Read a line
299                chomp(my $input = <CLIENT>);
300
301                # Restore alarm
302                alarm(0);
303                local $SIG{ALRM} = 'DEFAULT';
304
305                # Respond to request
306                print CLIENT "$header\n" if defined $header;
307                print CLIENT $copyright;
308               
309                my $lookup = lookup($input);
310                if (ref($lookup) eq 'ARRAY') {
311                        foreach my $entry (@$lookup) {
312                                my ($key, $value) = %$entry;
313                                print CLIENT outpad($key, $value);
314                        }
315                } elsif (ref($lookup) eq 'SCALAR') {
316                        print CLIENT $$lookup;
317                }
318                print CLIENT "\n";
319                print CLIENT "$footer\n" if defined $footer;
320
321                # We're done, close socket and exit child
322                close (CLIENT);
323                exit;
324        } continue { 
325                ### Parent
326                # Close CLIENT socket, useless to parent
327                close(CLIENT);
328        }
329}
330
331# Cleanup: close open handles etc.
332sub cleanup() {
333        close SERVER;
334        closelog if $daemon;
335        unlink $pidfile if $daemon;
336}
337
338# Lookups an IP/Domain in the database
339# Returns a reference to an array
340#  or, in case of an error, a reference to a scalar with the error message
341sub lookup($) {
342        my $qr = shift;
343
344        # Input validation
345        return \qq/Malformed request "$qr", please try again.\n/
346                if ($qr =~ /[ @]/);
347
348        # Query is IP address or not
349        my $isip = isIP($qr);
350
351        # Connect to MySQL
352        my $dbh = DBI->connect("DBI:mysql:$database{db}:$database{host}",
353                                "$database{user}", "$database{password}",
354                                {PrintError => !$daemon} )
355                or return \msg('warning',
356                   "Error '$DBI::err' while connecting to the database.\n");
357
358        # Set connection character set (MySQL 4.1+, no error handling)
359        $dbh->do("SET NAMES '$database{charset}';");
360
361        my ($querystr, $query);
362        # Are we searching for domains or IP ranges?
363        if ($isip) {
364                my $longip = ip2long($qr);
365
366                $querystr = qq/
367                SELECT  ipr.date_in AS date,
368                        nodes.name AS nodename,
369                        users.username AS owner,
370                        ipr.node_id AS id,
371                        areas.name AS area,
372                        regions.name AS region,
373                        ipr.ip_start AS longip_start,
374                        ipr.ip_end AS longip_end
375                                FROM            ip_ranges AS ipr
376                                LEFT JOIN       nodes
377                                        ON ipr.node_id = nodes.id
378                                LEFT JOIN       areas
379                                        ON nodes.area_id = areas.id
380                                LEFT JOIN       regions
381                                        ON areas.region_id = regions.id
382                                LEFT JOIN       users_nodes AS un
383                                        ON nodes.id = un.node_id
384                                        AND un.owner='Y'
385                                LEFT JOIN       users
386                                        ON users.id = un.user_id AND
387                                           users.status='activated'
388                                WHERE   ipr.status = 'active' AND
389                                        ipr.ip_start <= $longip AND
390                                        ipr.ip_end >= $longip; /;
391        } else {
392                # Remove trailing dot (.) if exists
393                $qr =~ s/\.?$//;
394
395                # Strip off forward zone suffix
396                $qr =~ s/\.$zone{forward}$//;
397
398                my $domain = $dbh->quote($qr);
399                $querystr = qq/
400                        SELECT zone.date_in AS date,
401                        nodes.name AS nodename,
402                        users.username AS owner,
403                        zone.node_id AS id,
404                        areas.name AS area,
405                        regions.name AS region,
406                        CONCAT(ns.name, ".", nsowner.name_ns) AS ns_prefix
407                                FROM            dns_zones AS zone
408                                LEFT JOIN       nodes
409                                        ON zone.node_id = nodes.id
410                                LEFT JOIN       areas
411                                        ON nodes.area_id = areas.id
412                                LEFT JOIN       regions
413                                        ON areas.region_id = regions.id
414                                LEFT JOIN       dns_zones_nameservers AS zn
415                                        ON zone.id = zn.zone_id
416                                LEFT JOIN       dns_nameservers AS ns
417                                        ON zn.nameserver_id = ns.id AND
418                                           ns.status='active'
419                                LEFT JOIN       nodes AS nsowner
420                                        ON ns.node_id = nsowner.id
421                                LEFT JOIN       users_nodes AS un
422                                        ON nodes.id = un.node_id AND
423                                           un.owner='Y'
424                                LEFT JOIN       users
425                                        ON users.id = un.user_id AND
426                                           users.status='activated'
427                                WHERE   zone.status = 'active' AND
428                                        zone.name = $domain; /;
429        }
430        $query = $dbh->prepare($querystr);
431        $query->execute
432                or return \msg('warning',
433                   "Error '$DBI::err' while querying the database.\n");
434
435        my $found;
436        # Common for both IP & Domain
437        my ($date, $nodename, $owner, $id, $area, $region);
438        my ($network, @ns);
439        my $webpage;
440        while (my @row = $query->fetchrow_array) {
441                # The first row contains everything we need...
442                unless ($found) {
443                        $found = 1;
444                        ($date, $nodename, $owner, $id, $area, $region) = @row;
445                        $network = formatipr(pop(@row), pop(@row)) if $isip;
446                        if (defined $website) {
447                                $webpage = sprintf($website, $id);
448                        }
449                }
450                # ...except the secondary nameservers
451                unless ($isip) {
452                        push(@ns, pop(@row));
453                }
454        }
455        # Disconnect from the database
456        $dbh->disconnect;
457
458        # Return if no match
459        return \"No match for \"$qr\".\n" unless $found;
460       
461        my @response;
462        if ($isip) {
463                push(@response, {'Network' => $network});
464        } else {
465                push(@response, {'Domain Name' => "$qr.$zone{forward}"});
466        }
467        push(@response, {'Creation Date' => $date});
468        push(@response, {'Node' => "$nodename (#$id)"});
469        push(@response, {'Node Owner' => $owner});
470        push(@response, {'Area/Region' => "$area, $region"});
471
472        foreach my $ns (@ns) {
473                push(@response, {'Name Server' => "$ns.$zone{ns}"}) if $ns;
474        }
475        push(@response, {'Website' => $webpage});
476
477        return \@response;
478}
479
480# Syslog-related functions
481sub msg($$) {
482        my ($level, $err) = ($_[0], $_[1]);
483        chomp($err);
484        syslog($level, $err) if $daemon;
485        # Return as entered; with or without newline
486        return $_[1];
487}
488
489# Checks if a string is a valid IP address
490sub isIP($) {
491        my $d = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])';
492        return ($_[0] =~ m/^$d\.$d\.$d\.$d$/);
493}
494
495# IP <-> LongIP conversions
496sub ip2long($) { return unpack('N',inet_aton(shift)); }
497sub long2ip($) { return inet_ntoa(pack 'N', shift); }
498
499# Format IP range output
500sub formatipr($$) {
501        my $start = shift;
502        my $end = shift;
503        ($start, $end) = ($end, $start) if $start > $end;
504        my $cidr = eval { log($end-$start+1)/log(2) };
505        if ($cidr and int($cidr) == $cidr) {
506                # It's a valid subnet, output '1.2.3.4/5'
507                my $network = ($start >> $cidr) << $cidr;
508                return long2ip($network).'/'.(32 - $cidr)
509                        if $start == $network;
510        }
511        # It's just a range, output '1.2.3.4 - 4.3.2.1'
512        return long2ip($start).' - '.long2ip($end);
513}
514
515# Formats output lines
516sub outpad($$) {
517        my $ret;
518        $ret .= "  $_[0]";
519        $ret .= '.' x (20 - length($_[0]));
520        $ret .= " $_[1]\n" if defined $_[1];
521}
522
523__END__
524
525=head1 NAME
526
527whoind - WiND WHOIS server
528
529=head1 SYNOPSIS
530
531whoind [options]
532
533=head1 OPTIONS
534
535=over 8
536
537=item B<-d, --daemon>
538
539Fork to the background
540
541=item B<-no-d, --no-daemon>
542
543Don't fork to the background (default)
544
545=item B<-p, --port>
546
547Port to listen to (default: 43)
548
549=item B<-u, --user>
550
551User to drop privileges to (default: nobody)
552
553=item B<-g, --group>
554
555Group to drop privileges to (default: nogroup)
556
557=item B<-c, --conf, --config>
558
559Configuration file path (default: /etc/whoind.conf)
560
561=item B<-c, --conf, --config>
562
563pidfile path (default: /var/run/whoid.pid)
564
565=item B<-h, --help>
566
567Show usage
568
569=back
570
571=head1 DESCRIPTION
572
573B<whoind> is a simple WHOIS protocol server intended to serve data from
574the WiND (Wireless Nodes Database) database.
575
576=head1 AUTHOR AND COPYRIGHT
577
578whoind was written by Faidon Liambotis <faidon@cube.gr> for the WiND project.
579Report bugs to http://wind.cube.gr/
580
581Copyright (c) 2005 Faidon Liambotis <faidon@cube.gr>
582
583=head1 LICENSE
584
585This program is free software; you can redistribute it and/or modify
586it under the terms of the GNU General Public License as published by
587the Free Software Foundation; version 2 dated June, 1991.
588
589=cut
590
Note: See TracBrowser for help on using the repository browser.