File Coverage

blib/lib/Config/Hosts.pm
Criterion Covered Total %
statement 175 205 85.3
branch 53 80 66.2
condition 22 35 62.8
subroutine 13 13 100.0
pod 10 10 100.0
total 273 343 79.5


line stmt bran cond sub pod time code
1             package Config::Hosts;
2              
3 7     7   169272 use warnings;
  7         18  
  7         853  
4 7     7   40 use strict;
  7         11  
  7         4141  
5              
6 7     7   8688 use Regexp::IPv6 qw($IPv6_re);
  7         7468  
  7         20274  
7              
8             =head1 NAME
9              
10             Config::Hosts - Interface to /etc/hosts file
11              
12             =head1 VERSION
13              
14             Version 0.01
15              
16             =cut
17              
18             our $VERSION = '0.03';
19              
20             our $DEFAULT_FILE = '/etc/hosts';
21              
22             our $TYPE_IP = 1;
23             our $TYPE_HOST = -1;
24              
25             =head1 SYNOPSIS
26              
27             Config::Hosts - Interface to /etc/hosts file. A tool that manages the
28             hosts list on a machine, is able to query/insert/delete/update the
29             entries by IP or by a hostname, and also maintains the original
30             comments and some sanity checks on IP and hostname values.
31              
32             use Config::Hosts;
33              
34             my $hosts = Config::Hosts->new();
35             $hosts->read_hosts(); # reads default /etc/hosts
36             $hosts->query_host($host_or_ip);
37             $hosts->insert_host(ip => $ip, hosts => [qw(host1 host2)]);
38             $hosts->update_host($ip, hosts=> [qw(host1 host3)]);
39             $hosts->delete_host('host3');
40             $hosts->write_hosts("/tmp/hosts");
41              
42             =head1 EXPORT
43              
44             The interface is entirely object-oriented. The following methods have
45             been defined:
46              
47             =head1 SUBROUTINES/METHODS
48              
49             =head2 new ($;%)
50              
51             The constructor. Accepts optional hash with one key only: file - the
52             name of the file as alternative to default /etc/hosts.
53              
54             Returns the newly blessed object.
55              
56             =cut
57              
58             sub new ($;@) {
59              
60 7     7 1 577 my $class = shift;
61 7         30 my %params = @_;
62              
63 7         19 my $self = {};
64 7   66     47 $self->{_file} = $params{file} || $DEFAULT_FILE;
65 7         19 bless $self, $class;
66              
67 7         24 return $self;
68             }
69              
70             =head2 is_valid_ip ($)
71              
72             internal utility function to check whether the IP given is a valid
73             IPv4 or IPv6 address. Returns 1 or 0, naturally.
74              
75             =cut
76              
77             sub is_valid_ip ($) {
78              
79 99     99 1 8520 my $ip = shift;
80 99 50       4234 if ($ip =~ /^$IPv6_re$/) {
81 0         0 return 1;
82             }
83 99 100 100     1014 return $ip =~
84             /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && ($1+0 | $2+0 | $3+0 | $4+0) < 0x100 ?
85             1 : 0;
86             }
87              
88             =head2 is_valid_host($)
89              
90             Internal utility to determine whether the host name is a valid
91             hostname as required by /etc/hosts manual.
92              
93             =cut
94              
95             sub is_valid_host ($) {
96              
97 83     83 1 102 my $host = shift;
98 83 100       668 return $host =~
99             /^[a-z]([a-z]|[0-9]|\-|\.)*([a-z]|[0-9])$/i ?
100             1 : 0;
101             }
102              
103             =head2 read_hosts($;$)
104              
105             Read the host file into a data structure to later be used by the other
106             methods. Optional argument may be the file to read hosts table from.
107              
108             =cut
109              
110             sub read_hosts ($;$) {
111              
112 6     6 1 43 my $self = shift;
113 6   33     29 my $hosts_file = shift || $self->{_file};
114              
115 6         12 my $contents = [];
116 6 50       283 open(H, $hosts_file) or die "Couldn't open hosts file $hosts_file: $!";
117 6         17 my $i = 0; my $l = 0;
  6         11  
118 6         13 my $hosts = {};
119 6         409 while () {
120 18         35 chomp;
121 18         21 $l++;
122 18 50 33     178 if (! /\S/) {
    50          
    50          
123 0         0 $contents->[$i] = $_;
124             }
125             elsif (/^\s*\#/) {
126 0         0 $contents->[$i] = $_;
127             }
128             elsif (
129             /^\s*(\d+\.\d+\.\d+\.\d+)\s+(\S.*)/ ||
130             /^\s*(\S*\:\S*\:\S*)\s+(\S.*)/
131             ) {
132 18         56 $_ = lc $_;
133 18         31 $contents->[$i] = $_;
134 18         40 my $ip = $1;
135 18         60 my ($hosts_list, $comment) = split(/\#/, $2);
136 18   50     83 $comment ||= "";
137 18 50       38 if (!is_valid_ip($ip)) {
138 0         0 print STDERR "Line $l: Warning: IP $ip is invalid\n";
139             }
140 18         56 my @hosts = split(/\s+/, $hosts_list);
141 18         35 $contents->[$i] = $_;
142 18 50       50 if ($hosts->{$ip}) {
143 0         0 print STDERR "Line $l: Warning: duplicate IP entry $ip, the last one will be used\n";
144             }
145 18         678 $hosts->{$ip} = {
146             hosts => [ @hosts ],
147             comment => $comment,
148             line => $i,
149             };
150 18         53 for my $host (@hosts) {
151 24 50       56 if (! is_valid_host($host)) {
152 0         0 print STDERR "Line $l: Warning: Host $host is invalid\n";
153             }
154 24 50       61 if ($hosts->{$host}) {
155 0         0 print STDERR "Line $l: Warning: duplicate Host entry $host, the last one will be used\n";
156             }
157 24         127 $hosts->{$host} = {
158             ip => $ip,
159             comment => $comment,
160             line => $i,
161             }
162             }
163             }
164             else {
165 0         0 die "Invalid entry: $_\nBailing out.\n";
166             }
167 18         96 $i++;
168             }
169 6         38 $self->{_contents} = $contents;
170 6         27 $self->{_hosts} = $hosts;
171             }
172              
173             =head2 determine_ip_or_host ($$)
174              
175             Check whether the given argument is an IP, a HOST or neither. Returns
176             1, -1 or 0 correspondingly.
177              
178             =cut
179              
180             sub determine_ip_or_host ($$) {
181              
182 59     59 1 12959 my $self = shift;
183 59         73 my $candidate = shift;
184              
185 59 100       116 return $TYPE_IP if is_valid_ip($candidate);
186 37 100       197 return $TYPE_HOST if is_valid_host($candidate);
187 1         3 return 0;
188             }
189              
190             =head2 query_host ($$)
191              
192             Queries the read hosts table to find specified argument that may be IP
193             address or host name.
194              
195             Returns hash containing the relevant entry if found or undef if not.
196              
197             =cut
198              
199             sub query_host ($$) {
200              
201 38     38 1 12267 my $self = shift;
202 38         50 my $host = shift;
203              
204 38         72 my $type = $self->determine_ip_or_host($host);
205 38 100       78 if ($type) {
206 37         116 return $self->{_hosts}{$host};
207             }
208             else {
209 1         4 return undef;
210             }
211             }
212              
213             =head2 insert_host ($%)
214              
215             Inserts a host. Both IP and hostnames must be specified as a hash.
216             Hostname may be a single scalar or arrayref of hostnames.
217              
218             =cut
219              
220             sub insert_host ($%) {
221              
222 6     6 1 1608 my $self = shift;
223 6         20 my %params = @_;
224              
225 6 50 33     38 if (! $params{ip} || ! $params{hosts}) {
226 0         0 print STDERR "No ip or host supplied to insert_host, ignoring\n";
227 0         0 return 0;
228             }
229 6         12 my $ip = $params{ip};
230 6 50       14 if (! is_valid_ip($ip)) {
231 0         0 print STDERR "Invalid IP $ip, ignoring\n";
232 0         0 return 0;
233             }
234 6         59 my $hosts;
235 6 100       23 if (! ref $params{hosts}) {
236 1         3 $hosts = [ $params{hosts} ];
237             }
238             else {
239 5         10 $hosts = $params{hosts};
240             }
241 6 50       20 if (ref $hosts ne 'ARRAY') {
242 0         0 print STDERR "Host names must be scalar value or ARRAY ref, ignoring\n";
243 0         0 return 0;
244             }
245 6 50       10 if (grep {
  11 50       21  
246 6         13 !is_valid_host($_) &&
247             print STDERR "Invalid host $_ passed, ignoring insert\n"
248             } @{$hosts}) {
249 0         0 return 0;
250             }
251 6         10 my $hosts_line = join(" ", @{$hosts});
  6         17  
252 6 50       22 my $comment = $params{comment} ? " $params{comment}" : "";
253 6         8 push(@{$self->{_contents}}, "$ip\t$hosts_line$comment");
  6         25  
254 6 100       578 if ($self->{_hosts}{$ip}) {
255 1         177 print STDERR "INSERT: Warning:duplicate IP $ip, the last one will be used\n";
256 1         3 for my $h_host (@{$self->{_hosts}{$ip}{hosts}}) {
  1         6  
257 2         9 delete $self->{_hosts}{$h_host}
258             }
259 1         3 my $index = $self->{_hosts}{$ip}{line};
260 1         3 splice(@{$self->{_contents}}, $index, 1);
  1         4  
261             }
262 6         670 $self->{_hosts}{$ip} = {
263             hosts => $hosts,
264             comment => $comment,
265 6         544 line => scalar @{$self->{_contents}},
266             };
267 6         14 for my $host (@{$hosts}) {
  6         14  
268 11 50       33 if ($self->{_hosts}{$host}) {
269 0         0 print STDERR "INSERT: Warning:duplicate Host entry $host, the last one will be used\n";
270             }
271 11         56 $self->{_hosts}{$host} = {
272             ip => $ip,
273             comment => $comment,
274 11         15 line => scalar @{$self->{_contents}},
275             }
276             }
277 6         24 return 1;
278             }
279              
280             =head2 delete_host ($$)
281              
282             Deletes an entry in hosts table. The entry is determined either by IP
283             or by hostname, all entries related to this host or IP are wiped out.
284              
285             =cut
286              
287             sub delete_host ($$) {
288              
289 3     3 1 12 my $self = shift;
290 3         4 my $host = shift;
291              
292 3         9 my $type = $self->determine_ip_or_host($host);
293 3 50       8 if (! $type) {
294 0         0 print STDERR "Invalid host $host supplied\n";
295 0         0 return 0;
296             }
297 3 50       9 if (! $self->{_hosts}{$host}) {
298 0         0 print STDERR "No such host $host\n";
299 0         0 return 0;
300             }
301 3         7 my $index = $self->{_hosts}{$host}{line};
302 3         3 splice(@{$self->{_contents}}, $index, 1);
  3         7  
303 3 100       7 if ($type == $TYPE_IP) {
304 1         2 for my $h_host (@{$self->{_hosts}{$host}{hosts}}) {
  1         3  
305 2         7 delete $self->{_hosts}{$h_host}
306             }
307 1         3 delete $self->{_hosts}{$host};
308             }
309             else {
310 2         5 my $ip = $self->{_hosts}{$host}{ip};
311 2         4 my $ip_hosts = $self->{_hosts}{$ip}{hosts};
312 2         3 for my $h_host (@{$ip_hosts}) {
  2         4  
313 4         13 delete $self->{_hosts}{$h_host}
314             }
315 2         6 delete $self->{_hosts}{$ip};
316             }
317 3         7 return 1;
318             }
319              
320             =head2 update_host ($$)
321              
322             Updates an entry in hosts table. Arguments should be of the following
323             format: $self->update_host($ip_or_host, ip => $new_ip, hosts => [
324             @new_hosts ]);
325              
326             New hosts' argument may be a single scalar instead of arrayref.
327              
328             =cut
329              
330             sub update_host ($$%) {
331              
332 4     4 1 836 my $self = shift;
333 4         8 my $host = shift;
334 4         9 my %params = @_;
335              
336 4         10 my $type = $self->determine_ip_or_host($host);
337 4 50       10 if (! $type) {
338 0         0 print STDERR "Invalid host $host supplied\n";
339 0         0 return 0;
340             }
341 4 50       12 if (! $self->{_hosts}{$host}) {
342 0         0 print STDERR "No such host $host\n";
343 0         0 return 0;
344             }
345 4         10 my $index = $self->{_hosts}{$host}{line};
346 4 50       8 my $comment = $params{comment} ? " $params{comment}" : "";
347 4         5 my $new_ip = $host;
348 4 50 66     15 if ($params{ip} && !is_valid_ip($params{ip})) {
349 0         0 print STDERR "Invalid argument IP given\n";
350 0         0 return 0;
351             }
352 4 100       11 if ($params{hosts}) {
353 3 50       8 if (! ref $params{hosts}) {
354 0         0 $params{hosts} = [ $params{hosts} ];
355             }
356 3 50       8 if (ref $params{hosts} ne 'ARRAY') {
357 0         0 print STDERR "New host names should be scalar value or array ref\n";
358 0         0 return 0;
359             }
360 3 50       4 if (grep {
  6 50       11  
361 3         7 !is_valid_host($_) &&
362             print STDERR "Invalid host $_ passed, ignoring insert\n"
363             } @{$params{hosts}}) {
364 0         0 return 0;
365             }
366             }
367 4 100 100     17 if ($type == $TYPE_IP && $params{ip}) {
368 1         3 $new_ip = $params{ip};
369 1         4 $self->{_hosts}{$new_ip} = delete $self->{_hosts}{$host};
370 1         2 for my $h_host (@{$self->{_hosts}{$new_ip}{hosts}}) {
  1         4  
371 2         5 $self->{_hosts}{$h_host}{ip} = $new_ip;
372             }
373             }
374 4 100 66     18 if ($type == $TYPE_IP && $params{hosts}) {
375 2         1 my @old_hosts = @{$self->{_hosts}{$new_ip}{hosts}};
  2         7  
376 2         37 $self->{_hosts}{$new_ip}{hosts} = $params{hosts};
377 2         5 for my $old_host (@old_hosts) {
378 4         13 delete $self->{_hosts}{$old_host};
379             }
380 2         3 for my $new_host (@{$self->{_hosts}{$new_ip}{hosts}}) {
  2         6  
381 4         21 $self->{_hosts}{$new_host} = {
382             ip => $new_ip,
383             comment => $comment,
384             line => $self->{_hosts}{$new_ip}{line},
385             }
386             }
387             }
388 4 100 66     17 if ($type == $TYPE_HOST && $params{ip}) {
389 2         5 my $old_ip = $self->{_hosts}{$host}{ip};
390 2         3 $new_ip = $params{ip};
391 2         3 for my $h_host (@{$self->{_hosts}{$old_ip}{hosts}}) {
  2         5  
392 4         11 $self->{_hosts}{$h_host}{ip} = $new_ip;
393             }
394 2         7 $self->{_hosts}{$new_ip} = delete $self->{_hosts}{$old_ip};
395             }
396 4 100 100     17 if ($type == $TYPE_HOST && $params{hosts}) {
397 1         3 $new_ip = $self->{_hosts}{$host}{ip};
398 1         2 my @old_hosts = @{$self->{_hosts}{$new_ip}{hosts}};
  1         4  
399 1         3 $self->{_hosts}{$new_ip}{hosts} = $params{hosts};
400 1         2 for my $old_host (@old_hosts) {
401 2         6 delete $self->{_hosts}{$old_host};
402             }
403 1         2 for my $new_host (@{$self->{_hosts}{$new_ip}{hosts}}) {
  1         3  
404 2         11 $self->{_hosts}{$new_host} = {
405             ip => $new_ip,
406             comment => $comment,
407             line => $self->{_hosts}{$new_ip}{line},
408             }
409             }
410             }
411 4         6 my $hosts_line = join(" ", @{$self->{_hosts}{$new_ip}{hosts}});
  4         12  
412 4         10 my $new_line = "$new_ip\t$hosts_line$comment";
413 4         4 splice(@{$self->{_contents}}, $index, 1, $new_line);
  4         11  
414              
415 4         14 return 1;
416             }
417              
418             =head2 write_hosts($;$)
419              
420             Writes the hosts table either to the default or to a specified (via
421             parameter) file.
422              
423             =cut
424              
425             sub write_hosts ($;$) {
426              
427 1     1 1 438 my $self = shift;
428 1   33     27 my $hosts_file = shift || $self->{_file};
429              
430 1 50       168 open(F, ">$hosts_file") or die "Cannot write hosts file $hosts_file: $!";
431 1         3 local $, = "\n";
432 1         3 local $\ = "\n";
433 1         2 print F @{$self->{_contents}};
  1         19  
434 1         51 close F;
435             }
436              
437             =head1 AUTHOR
438              
439             Roman M. Parparov, C<< >>
440              
441             =head1 BUGS
442              
443             Please report any bugs or feature requests to C
444             rt.cpan.org>, or through the web interface at
445             L. I
446             will be notified, and then you'll automatically be notified of
447             progress on your bug as I make changes.
448              
449             CAVEAT: the changes in host table are not committed unless you
450             explicitly write_hosts() them.
451              
452             =head1 SUPPORT
453              
454             You can find documentation for this module with the perldoc command.
455              
456             perldoc Config::Hosts
457              
458              
459             You can also look for information at:
460              
461             =over 4
462              
463             =item * RT: CPAN's request tracker
464              
465             L
466              
467             =item * AnnoCPAN: Annotated CPAN documentation
468              
469             L
470              
471             =item * CPAN Ratings
472              
473             L
474              
475             =item * Search CPAN
476              
477             L
478              
479             =back
480              
481              
482             =head1 ACKNOWLEDGEMENTS
483              
484             Thanks to Vicente Gavara C<< >> for
485             providing a fix for editing/deleting routines.
486              
487             =head1 LICENSE AND COPYRIGHT
488              
489             Copyright 2011 Roman M. Parparov.
490              
491             This program is free software; you can redistribute it and/or modify it
492             under the terms of either: the GNU General Public License as published
493             by the Free Software Foundation; or the Artistic License.
494              
495             See http://dev.perl.org/licenses/ for more information.
496              
497              
498             =cut
499              
500             1; # End of Config::Hosts