File Coverage

blib/lib/Captive/Portal/Role/Utils.pm
Criterion Covered Total %
statement 103 142 72.5
branch 27 64 42.1
condition 11 20 55.0
subroutine 15 15 100.0
pod 6 6 100.0
total 162 247 65.5


line stmt bran cond sub pod time code
1             package Captive::Portal::Role::Utils;
2              
3 8     8   25916 use strict;
  8         17  
  8         355  
4 8     8   38 use warnings;
  8         16  
  8         450  
5              
6             =head1 NAME
7              
8             Captive::Portal::Role::Utils - common utils for Captive::Portal
9              
10             =cut
11              
12             our $VERSION = '4.10';
13              
14 8     8   2432 use Log::Log4perl qw(:easy);
  8         127878  
  8         82  
15 8     8   27884 use Spawn::Safe qw(spawn_safe);
  8         69223  
  8         1675  
16 8     8   99 use Try::Tiny;
  8         17  
  8         627  
17 8     8   21974 use Socket qw(inet_ntoa);
  8         49347  
  8         2217  
18 8     8   11109 use Net::hostent;
  8         76005  
  8         72  
19 8     8   2860 use Template::Exception;
  8         1797  
  8         232  
20              
21 8     8   2233 use Role::Basic;
  8         39126  
  8         88  
22             requires qw(cfg);
23              
24             =head1 DESCRIPTION
25              
26             Utility roles needed by other modules. All roles die on error.
27              
28             =head1 ROLES
29              
30             =over 4
31              
32             =item $capo->find_mac($ip)
33              
34             Returns the corresponding MAC address for given IP address from /proc/net/arp on success or undef on failure.
35              
36             =cut
37              
38             sub find_mac {
39 3     3 1 9 my $self = shift;
40 3 50       21 my $lookup_ip = shift
41             or LOGDIE("missing parameter 'ip'");
42              
43 3 50       12 if ( $self->cfg->{MOCK_MAC} ) {
44 3         16 DEBUG 'using mocked MAC address';
45 3         32 return 'DE:AD:BE:EF:DE:AD';
46             }
47              
48 0         0 DEBUG 'open /proc/net/arp';
49              
50 0 0       0 open ARP, '<', '/proc/net/arp'
51             or LOGDIE "Couldn't open /proc/net/arp: $!\n";
52              
53 0 0       0 my @proc_net_arp =
54             or LOGDIE "Couldn't read /proc/net/arp: $!\n";
55              
56             # regex for ipv4 address
57 0         0 my $ipv4_rx = qr/\d{1,3} \. \d{1,3} \. \d{1,3} \. \d{1,3}/x;
58              
59             # regex for MAC address matching
60 0         0 my $hex_digit_rx = qr/[A-F,a-f,0-9]/;
61 0         0 my $mac_rx = qr/(?:$hex_digit_rx{2}:){5} $hex_digit_rx{2}/x;
62              
63 0         0 my $arp_tbl = {};
64 0         0 foreach my $line (@proc_net_arp) {
65              
66             # 10.10.1.2 0x1 0x2 00:00:01:02:03:04 * eth0
67              
68 0         0 my ( $ip, $mac ) = (
69             $line =~ m/
70             ^
71             ($ipv4_rx) # IP-addr
72             \s+ 0x\d+ \s+ 0x2 \s+
73             ($mac_rx) # MAC-addr
74             \s+ .*
75             /x
76             );
77              
78             # arp flag 0x02 invalid or parse error
79 0 0 0     0 next unless defined $ip && defined $mac;
80              
81 0         0 $ip = $self->normalize_ip($ip);
82 0         0 $arp_tbl->{$ip} = uc $mac;
83             }
84              
85 0         0 my $mac = $arp_tbl->{$lookup_ip};
86              
87 0 0       0 return $mac if $mac;
88              
89             # nothing found
90 0         0 DEBUG "can't find ip in ARPTABLE: '$lookup_ip'";
91              
92 0         0 return;
93             }
94              
95             =item $capo->ip2hex($ip)
96              
97             Helper method, convert ipv4 address to hexadecimal representation.
98              
99             Example:
100             '10.1.2.254' -> '0a0102fe'
101              
102             =cut
103              
104             sub ip2hex {
105 2     2 1 4 my $self = shift;
106 2 50       9 my $ip = shift
107             or LOGDIE 'missing param ip';
108              
109 2         32 return unpack( 'H8', pack( 'C4', split( /\./, $ip ) ) );
110             }
111              
112             =item $capo->normalize_ip($ip)
113              
114             Helper method, normalize ip adresses, strip leading zeros in octets.
115              
116             Example:
117             '012.2.3.000' -> '12.2.3.0'
118              
119             =cut
120              
121             sub normalize_ip {
122 6     6 1 1678 my $self = shift;
123              
124 6 50       101 my $ip = shift
125             or LOGDIE "FATAL: missing param 'ip',";
126              
127 6         33 my @octets = split /\./, $ip;
128              
129 6 50       24 LOGDIE "FATAL: couldn't split '$ip' into 4 octets,"
130             if scalar @octets != 4;
131              
132             # delete leading zeros in octets
133             # (side effect: wrap octets 256 -> 0, ...), should not happen
134 6         158 my $ip_packed_unpacked = join '.', unpack 'C4', pack 'C4', @octets;
135              
136 6         97 return $ip_packed_unpacked;
137             }
138              
139             =item $capo->drop_privileges()
140              
141             Running under root, like normal cronjobs do, should drop to the same uid/gid as the http daemon (and fcgi script). uid/gid is taken from config as RUN_USER/RUN_GROUP.
142              
143             =cut
144              
145             sub drop_privileges {
146 5     5 1 16 my $self = shift;
147              
148 5 50       36 my $user = $self->cfg->{RUN_USER}
149             or LOGDIE "FATAL: missing 'RUN_USER' in cfg file,";
150              
151 5 50       39 my $group = $self->cfg->{RUN_GROUP}
152             or LOGDIE "FATAL: missing 'RUN_GROUP' in cfg file,";
153              
154 5         59 DEBUG "drop privileges to $user:$group";
155              
156             ########
157             # resolve user to username and/or uid
158 5         47 my ( $uname, $uid );
159              
160 5 50       43 if ( $user =~ m/^\d+$/ ) {
161 0         0 $uname = getpwuid($user);
162 0         0 $uid = $user;
163             }
164             else {
165 5         348 $uid = getpwnam($user);
166 5         17 $uname = $user;
167             }
168              
169 5 50 33     53 unless ( defined($uname) and defined($uid) ) {
170 0         0 LOGDIE "user '$user' not known to system\n";
171             }
172              
173             ########
174             # resolve group to groupname and/or gid
175 5         14 my ( $gname, $gid );
176              
177 5 50       26 if ( $group =~ m/^\d+$/ ) {
178 0         0 $gname = getgrgid($group);
179 0         0 $gid = $group;
180             }
181             else {
182 5         368 $gid = getgrnam($group);
183 5         18 $gname = $group;
184             }
185              
186 5 50 33     129 unless ( defined($gname) and defined($gid) ) {
187 0         0 LOGDIE "group '$group' not known to system\n";
188             }
189              
190             # switch to user:group not needed
191             # already running under required uid:gid
192 5 50 33     106 return if $> == $uid && $) == $gid;
193              
194 0         0 DEBUG "switch GID and EGID to $gid";
195              
196 0         0 $( = $) = $gid;
197 0 0       0 LOGDIE "cannot change group to '$group': $!\n"
198             if $) != $gid;
199              
200 0         0 DEBUG "switch UID and EUID to $uid";
201              
202 0         0 $< = $> = $uid;
203 0 0       0 LOGDIE "cannot change user to '$user': $!\n"
204             if $> != $uid;
205              
206             }
207              
208             =item $capo->spawn_cmd(@cmd_with_options, [$spawn_cmd_options])
209              
210             Wrapper to run external commands, capture and return (stdout/stderr).
211              
212             Last optional parameter item is a hashref with options for spawn_cmd itself:
213              
214             {
215             timeout => 2, # default 2s
216             ignore_exit_codes => [], # exit codes without exception
217             }
218              
219             If the external command doesn't return after I, the command is interrupted and an exception is thrown.
220              
221             Exit codes != 0 and not defined in I throw exceptions.
222              
223             =cut
224              
225             sub spawn_cmd {
226 10     10 1 14821 my $self = shift;
227 10         60 my @argv = @_;
228 10 50       43 LOGDIE "Paramter missing," unless scalar @argv;
229              
230             # defaults
231 10         49 my $options = {
232             timeout => 2, # at least 2s !
233             ignore_exit_codes => [],
234             };
235              
236             # options from caller override defaults
237 10 100       51 if ( ref $argv[-1] eq 'HASH' ) {
238 2         116 $options = { %$options, %{ pop @argv } };
  2         10  
239             }
240              
241 10         19 my $results;
242              
243 10         72 DEBUG("try to spawn: @argv");
244             {
245             ####
246             # get rid of some limitations with FCGI
247             # ERROR: "Not a GLOB reference at .../FCGI.pm line 125"
248              
249 10         103 local *STDIN;
  10         31  
250 10         28 local *STDOUT;
251 10         22 local *STDERR;
252              
253 10 50       466 open( STDIN, '<&=0' ) or die $!;
254 10 50       157 open( STDOUT, '>>&=1' ) or die $!;
255 10 50       266 open( STDERR, '>>&=2' ) or die $!;
256              
257             #
258 10         92 $results = spawn_safe(
259             {
260             argv => [@argv],
261             timeout => $options->{timeout},
262             }
263             );
264             }
265 9         6655934 DEBUG("end of spawn: @argv");
266              
267             #################################
268              
269 9   100     380 my $exit_code = $results->{exit_code} || 0;
270 9   100     74 my $error = $results->{error} || '';
271 9   100     65 my $stdout = $results->{stdout} || '';
272 9   100     115 my $stderr = $results->{stderr} || '';
273              
274 9 100       41 if ($error) {
275 3         46 DEBUG "ERROR in spawning command: @argv";
276 3         43 DEBUG "... error: $error";
277 3         37 DEBUG "... exit_code: $exit_code";
278 3         37 DEBUG "... stdout: $stdout";
279 3         23 DEBUG "... stderr: $stderr";
280              
281 3         121 die "'$error' in spawning @argv\n";
282             }
283              
284             # something went wrong with exec, shall we ignore it
285 6 100       33 if ( $exit_code != 0 ) {
286              
287 4         16 die "'$stderr' in spawning @argv\n"
288 2 50       18 unless grep { $exit_code == $_ } @{ $options->{ignore_exit_codes} };
  2         26  
289              
290 2         52 DEBUG "ignored EXIT_CODE !=0 in spawning command: @argv";
291 2         18 DEBUG "... error: $error";
292 2         18 DEBUG "... exit_code: $exit_code";
293 2         16 DEBUG "... stdout: $stdout";
294 2         20 DEBUG "... stderr: $stderr";
295             }
296              
297 6         109 return ( $stdout, $stderr );
298             }
299              
300             =item $capo->ipv4_aton($hosts)
301              
302             Template callback converting DNS name(s) to ip address(es), see perldoc Template::Manual::Variables. With this helper, DNS-names in firewall templates are translated to ipv4 adresses.
303              
304             Example:
305              
306             '10.10.10.10' -> '10.10.10.10'
307             'www.acme.rog' -> [10.1.2.3, 10.1.2.4, 10.1.2.5, ...]
308             [ftp.uni-ulm.de, www.uni-ulm.de] -> [134.60.1.5, 134.60.1.25]
309              
310             =cut
311              
312             sub ipv4_aton {
313 1 50   1 1 6 my @hosts = @_
314             or
315             die Template::Exception->new( 'ipv4_aton', "missing param 'hosts'\n" );
316              
317             # explode array refs
318 1         3 my @host_list;
319 1         3 foreach my $host (@hosts) {
320 1 50       8 if ( not ref $host ) {
    50          
321 0         0 push @host_list, $host;
322             }
323             elsif ( ref $host eq 'ARRAY' ) {
324 1         3 push @host_list, @$host;
325             }
326             else {
327 0         0 die Template::Exception->new( 'ipv4_aton',
328             "param 'hosts' must be a SCALAR or ARRAY_REF\n" );
329             }
330             }
331              
332 1         3 my @addr_list = ();
333 1         2 foreach my $host (@host_list) {
334              
335             # got an IP address instead of DNS name
336 0 0       0 if ( $host =~ m/^[.0-9]+$/ ) {
337              
338             # push it to addr_list regardless of DNS entry
339 0         0 push @addr_list, $host;
340 0         0 next;
341             }
342              
343 0         0 my $hostent;
344 0 0       0 unless ( $hostent = gethost($host) ) {
345 0         0 die Template::Exception->new( 'ipv4_aton',
346             "No such host: '$host'\n" );
347             }
348              
349 0         0 foreach my $packed_ip ( @{ $hostent->addr_list } ) {
  0         0  
350 0         0 push @addr_list, inet_ntoa($packed_ip);
351             }
352             }
353              
354 1 50       9 scalar @addr_list == 1
355             ? return $addr_list[0]
356             : return \@addr_list;
357             }
358              
359             1;
360              
361             =back
362              
363             =head1 AUTHOR
364              
365             Karl Gaissmaier, C<< >>
366              
367             =head1 LICENSE AND COPYRIGHT
368              
369             Copyright 2010-2013 Karl Gaissmaier, all rights reserved.
370              
371             This distribution is free software; you can redistribute it and/or modify it
372             under the terms of either:
373              
374             a) the GNU General Public License as published by the Free Software
375             Foundation; either version 2, or (at your option) any later version, or
376              
377             b) the Artistic License version 2.0.
378              
379             =cut
380              
381             # vim: sw=4
382