File Coverage

blib/lib/Captive/Portal.pm
Criterion Covered Total %
statement 241 335 71.9
branch 61 128 47.6
condition 10 27 37.0
subroutine 20 27 74.0
pod 0 14 0.0
total 332 531 62.5


line stmt bran cond sub pod time code
1             package Captive::Portal;
2              
3 6     6   41081 use strict;
  6         13  
  6         254  
4 6     6   35 use warnings;
  6         12  
  6         475  
5              
6             our $VERSION = '4.10';
7              
8             =head1 NAME
9              
10             Captive::Portal - Perl based solution for controlled network access
11              
12             =head1 ABSTRACT
13              
14             A so called I written in perl for Linux Gateways. For a longer explanation see:
15              
16             L
17              
18             =head1 DESCRIPTION
19              
20             Captive::Portal a.k.a. CaPo is a hotspot solution for Linux Gateways. CaPo is developed and in service at Ulm University for thousands of concurrent users.
21              
22             The main focus is scalability, performance, simple administration and user-friendliness. The goals have been achieved by using scalable technologies like ipset/iptables, FastCGI/CGI and a fine tuned concurrent session handling based on the filesystem locking mechanism without any need for an additional RDBMS.
23              
24             CaPo is compatible with any FastCGI/CGI enabled HTTPS-server.
25              
26             =head1 ALGORITHM IN SHORT
27              
28             =over 4
29              
30             =item 1. Access Denied
31              
32             Only selected protocols like DHCP/DNS/NTP/IMAPS/IPSec/... to selected servers are allowed for unauthenticated clients.
33              
34             =item 2. Internal NAT redirect
35              
36             HTTP-traffic on the gateways inside interface - from unauthenticated clients - is redirected by an iptables(8) NAT-rule to a port the HTTP-server is listening, e.g.
37              
38             iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 80 -j REDIRECT --to-port 5281
39              
40             =item 3. External HTTP redirect
41              
42             The HTTP-server redirects the HTTP-request by a rewrite rule to an HTTPS-request for the CaPo script I , e.g.
43              
44            
45             RewriteEngine On
46             RewriteRule .* https://gateway.acme.org/capo/? [R,L]
47            
48              
49             CLIENT REQUEST:
50              
51             GET /foo/bar HTTP/1.1
52             Host: www.test.org
53              
54             SERVER RESPONSE:
55              
56             HTTP/1.1 302 Found
57             Location: https://gateway.acme.org/capo/
58              
59             =item 4. SESSION LOGIN
60              
61             The I script, fired due to this redirected request, offers a splash/login page. After successful login the firewall is dynamically changed to allow this clients IP/MAC tuple for internet access by ipset(8):
62              
63             ipset add capo_sessions_ipset CLIENT_IP,CLIENT_MAC
64              
65             =item 5. SESSION LOGOUT
66              
67             The capo.fcgi script offers a status/logout page. After successful logout the firewall is dynamically changed to disallow this IP/MAC tuple for internet access.
68              
69             ipset del capo_sessions_ipset CLIENT_IP
70              
71             =item 6. SESSION IDLE
72              
73             A cronjob fires periodically the capo-ctl.pl script checking for idle or malformed sessions. Idle means, the client didn't send any packet for a period of time (cfg param: IDLE_TIME = 10min). Clients sending packets are registered via iptables/ipset in the capo_activity_ipset.
74              
75             =item 7. COMFORTABLE SESSION REACTIVATION
76              
77             For a short period of time (cfg param: KEEP_OLD_STATE_PERIOD = 1h) the session is still on disc, but in IDLE state. If a client has cookies enabled and a HTTP request matches the stored IP/MAC/COOKIE data on disc, the session is reactivated without a login page.
78              
79             =back
80              
81             =head1 INSTALLATION
82              
83             Please see the INSTALL file in this distribution.
84              
85             =head1 CONFIGURATION
86              
87             The configuration file is searched in the following default places:
88              
89             $ENV{CAPTIVE_PORTAL_CONFIG} ||
90             $Bin/../etc/local/config.pl ||
91             $Bin/../etc/config.pl
92              
93             =head1 LOGGING
94              
95             Logging is handled by the Log::Log4perl module. The logging configuration is searched in the following default places:
96              
97             $ENV{CAPTIVE_PORTAL_LOG4PERL} ||
98             $Bin/../etc/local/log4perl.conf ||
99             $Bin/../etc/log4perl.conf
100              
101             =head1 LOCAL ADAPTATION
102              
103             The HTML files are generated from templates (Template-Toolkit syntax). You should use the original template files as stanzas and put the locally changed versions into the local template tree. The template search order prefers the local templates.
104              
105             The CSS is based on the wonderful blueprint css framework, see L. Of course you may use your own styles if needed.
106              
107             The firewall rules and commands are also generated from template files. Normally there is no need to change the firewall rules but it would be possible to add some local needed additional rules without changing the perl code. Be careful, you must understand the algorithm and the different states. Best you ask the author for any modifications.
108              
109             =head1 I18N
110              
111             There exists a template tree for each supported language. Some system/error messages used by the program must also be translated for the message catalog in the config file.
112              
113             =cut
114              
115 6     6   6178 use POSIX qw(strftime);
  6         142007  
  6         72  
116 6     6   54736 use Log::Log4perl qw(:easy);
  6         577244  
  6         51  
117 6     6   9095 use Try::Tiny;
  6         11981  
  6         1162  
118 6     6   6576 use Template;
  6         184166  
  6         255  
119              
120             # consume CaPo roles
121 6     6   7763 use Role::Basic qw(with);
  6         155150  
  6         57  
122             with qw(
123             Captive::Portal::Role::Config
124             Captive::Portal::Role::Utils
125             Captive::Portal::Role::I18N
126             Captive::Portal::Role::AuthenSimple
127             Captive::Portal::Role::Session
128             Captive::Portal::Role::Firewall
129             );
130              
131             #################################################
132             # create CaPo object once
133             #
134             # read the config
135             # drop privileges
136             # create Template object
137             # create authentication object
138             # open/create session dir
139             #
140             sub new {
141 8 50   8 0 7596 my $class = shift or LOGDIE "missing param 'class'\n";
142              
143             # create empty object
144 8         33 my $self = bless {}, $class;
145              
146 8         22 my $opts = {};
147 8 50 33     53 if ( ref $_[0] && ref $_[0] eq 'HASH' ) {
148 0         0 $opts = shift;
149             }
150             else {
151 8         39 %$opts = @_;
152             }
153              
154             # parse cfg file or use defaults
155 8 50       54 if ( $opts->{cfg_file} ) {
156 8         51 DEBUG('new(): parse cfg file');
157 8         127 $self->parse_cfg_file( $opts->{cfg_file} );
158             }
159              
160 5         31 DEBUG 'new(): drop privileges';
161 5         75 $self->drop_privileges;
162              
163 5         28 DEBUG 'new(): try to create Template object with INCLUDE_PATH: ',
164             join( ':', $self->cfg->{TEMPLATE_INCLUDE_PATH} );
165              
166 5 50       69 $self->{template} = Template->new(
167             { INCLUDE_PATH => $self->cfg->{TEMPLATE_INCLUDE_PATH}, } )
168             or LOGDIE "$Template::ERROR\n";
169              
170 5         189769 DEBUG 'new(): create Authen::Simple object';
171 5 50       112 $self->build_authenticator
172             or LOGDIE "Couldn't build Authen::Simple object\n";
173              
174             # check/create sessions-dir
175 5         42 DEBUG 'new(): check or create sessions-dir';
176 5         68 $self->open_sessions_dir;
177              
178 5         63 return $self; # CaPo object
179             }
180              
181             ##############################################
182             # run is the entry point for any http request
183             #
184             sub run {
185 7     7 0 185391 my $self = shift; # CaPo object
186              
187 7 50       41 my $query = shift or LOGDIE "run(): missing param 'query'\n";
188 7   100     180 my $path_info = $query->path_info || '';
189 7   50     2457 my $client_addr = $query->remote_addr || '?.?.?.?';
190              
191 7         201 DEBUG('------------- run(): REQUEST BEGIN --------------');
192              
193             # rip passwords from url for safe logging
194 7         241 my $safe_url = $query->self_url;
195 7         24185 $safe_url =~ s/password= .+? (;|\Z) /password=******;/x;
196 7         37 $safe_url =~ s/admin_secret= .+? (;|\Z) /admin_secret=******;/x;
197              
198 7         52 DEBUG "got request from $client_addr: $safe_url ...";
199 7         99 DEBUG "got path_info: $path_info";
200              
201 7         45 my $error;
202             try {
203              
204             ########
205             # reset this requests context with current request values
206             #
207 7     7   407 $self->{CTX} = {};
208 7         112 $self->{CTX}{QUERY} = $query;
209 7         275 $self->{CTX}{PATH_INFO} = $path_info;
210              
211 7         382 $self->{CTX}{HEADER} = $query->header(
212             -type => 'text/html',
213             -charset => 'UTF-8',
214             );
215 7         4576 $self->{CTX}{BODY} = '';
216 7         56 $self->{CTX}{LANG} = $self->choose_language;
217 7         65 DEBUG( 'choosen language: ' . $self->{CTX}{LANG} );
218              
219 7         61 $self->{CTX}{TMPL_VARS} = {};
220 7         29 $self->{CTX}{TMPL_VARS}{version} = $VERSION;
221 7         32 $self->{CTX}{TMPL_VARS}{ssl_required} = $self->cfg->{SSL_REQUIRED};
222              
223             ########
224             # start the dispatcher for this request
225             #
226 7         55 $self->dispatch;
227             }
228 7     0   117 catch { $error = $_ };
  0         0  
229              
230 7 50       1558 if ($error) {
231 0         0 WARN "catched error: $error";
232              
233 0         0 $self->{CTX}{BODY} = error_page_500($error);
234              
235 0         0 $self->{CTX}{HEADER} = $query->header(
236             -status => 500,
237             -type => 'text/html',
238             -charset => 'UTF-8',
239             );
240             }
241              
242             ########
243             # print this requests answer page
244             #
245 7         85 DEBUG('print http-header');
246 7         669 print $self->{CTX}{HEADER};
247              
248             # ... or LOGDIE "Couldn't print HTTP header";
249             # not possible, bug in older FCGI versions, sigh
250              
251 7         31 DEBUG('print http-body');
252 7         135 print $self->{CTX}{BODY};
253              
254             # ... or LOGDIE "Couldn't print HTTP body";
255             # not possible, bug in older FCGI versions, sigh
256              
257 7         25 DEBUG('------------- run(): REQUEST END ----------------');
258 7         81 return;
259             }
260              
261             ##############################################
262             # dispatch this request to the proper handler
263             # different actions can be requested by CGI parameters or path_info
264             #
265             # status: show a short status page
266             # is_running: show in plain text numbers of active sessions
267             # login: process login and show active page
268             # logout: process logout and show splash page
269             # .*: show splash page
270             #
271             sub dispatch {
272 7     7 0 15 my $self = shift;
273              
274 7         22 DEBUG 'running DISPATCH handler ...';
275              
276             # this requests parameters are in the context slot
277 7         54 my $query = $self->{CTX}{QUERY};
278 7         18 my $path_info = $self->{CTX}{PATH_INFO};
279              
280             ###############################################################
281             # first check for status requests
282             ###############################################################
283              
284             #############
285             # check if the status page is requested via path_info
286              
287 7 100       39 if ( $path_info =~ m/\b status \b/x ) {
288 3         16 return $self->summary_status_view;
289             }
290              
291             # or via cgi parameter
292 4 50       89 if ( exists $query->Vars->{status} ) {
293 0         0 return $self->summary_status_view;
294             }
295              
296             #############
297             # check if the is_running status page is requested via path_info
298              
299 4 100       725 if ( $path_info =~ m/\b is_running \b/x ) {
300 1         7 return $self->is_running_view;
301             }
302              
303             # or via cgi parameter
304 3 50       64 if ( exists $query->Vars->{is_running} ) {
305 0         0 return $self->is_running_view;
306             }
307              
308             ###############################################################
309             # now dispatch all the remaining client requests
310             ###############################################################
311              
312             #############
313             # stop client request if client MAC isn't available
314             # perhaps coming from wrong interface
315              
316 3 50       241 my $session = $self->get_current_session
317             or return $self->no_mac_view;
318              
319             # ok, got current session or created new on the fly
320 3         12 $self->{CTX}{SESSION} = $session;
321              
322             # missing SSL detected by JS, recorded from client via ajax
323             # check logfile for maybe man-in-the-middle attacks
324 3 50       91 return $self->no_ssl_detected
325             if exists $query->Vars->{no_ssl};
326              
327             # login requested
328 3 100       380 return $self->login
329             if exists $query->Vars->{login};
330              
331             # logout requested
332 2 100       311 return $self->logout
333             if exists $query->Vars->{logout};
334              
335             # first hit, no session established yet
336 1 50       94 return $self->splash_view
337             if $session->{STATE} eq 'init';
338              
339             # just a reload of an active session
340 0 0       0 return $self->active_view
341             if $session->{STATE} eq 'active';
342              
343             # reenable an idle session if the cookie is still valid
344 0 0 0     0 return $self->idle_view
345             if $session->{STATE} eq 'idle' && $self->match_cookie;
346              
347             # it's a reload after a logout or idle session,
348 0         0 $self->{CTX}{TMPL_VARS}{msg_text} =
349             $self->gettext('msg_001') . " $session->{STATE}";
350              
351 0         0 $self->{CTX}{TMPL_VARS}{msg_type} = 'info';
352              
353 0         0 return $self->splash_view;
354             }
355              
356             ##############################################
357             # no client MAC address found, show respective page
358             # we need client IP/MAC address tuple for login
359             #
360             sub no_mac_view {
361 0     0 0 0 my $self = shift;
362              
363 0         0 DEBUG('running NO_MAC handler ...');
364              
365             # this requests parameters are in the context slot
366 0         0 my $output = \$self->{CTX}{BODY};
367              
368 0         0 my $template = "view/$self->{CTX}{LANG}/nomac.tt";
369              
370 0 0       0 $self->{template}->process( $template, $self->{CTX}{TMPL_VARS}, $output )
371             or LOGDIE $self->{template}->error . "\n";
372             }
373              
374             ##############################################
375             # no ssl encryption recorded by client via ajax
376             #
377             sub no_ssl_detected {
378 0     0 0 0 my $self = shift;
379              
380 0         0 DEBUG('running NO_SSL handler ...');
381              
382 0         0 my $query = $self->{CTX}{QUERY};
383 0         0 my $session = $self->{CTX}{SESSION};
384              
385 0   0     0 my $ip = $session->{IP} || '';
386 0   0     0 my $mac = $session->{MAC} || '';
387              
388             # value is victim or aggressor
389 0         0 my $role = $query->Vars->{no_ssl};
390              
391 0         0 $self->{CTX}{HEADER} = $query->header(
392             -type => 'text/plain',
393             -charset => 'UTF-8',
394              
395             # maybe cross-domain-request from http -> https
396             -access_control_allow_origin => '*',
397             );
398              
399 0 0       0 if ( $role eq 'victim' ) {
    0          
400              
401             # JS ajax call via https
402 0         0 ERROR("maybe MITM victim IP/MAC: '$ip/$mac'");
403 0         0 $self->{CTX}{BODY} = $self->gettext('msg_007');
404             }
405             elsif ( $role eq 'mitm' ) {
406              
407             # JS ajax call via http,
408             # proxied from aggressor via https
409 0         0 ERROR("maybe MITM aggressor IP/MAC: '$ip/$mac'");
410 0         0 $self->{CTX}{BODY} = '';
411             }
412             else {
413              
414             # logic error
415 0         0 ERROR("MITM unknown role from JS: '$role'");
416             }
417              
418 0         0 return;
419             }
420              
421             ##############################################
422             # CLIENT API: no special action required, show splash page
423             #
424             sub splash_view {
425 2     2 0 4 my $self = shift;
426              
427 2         9 DEBUG('running SPLASH handler ...');
428              
429             # this requests parameters are in the context slot
430 2         21 my $output = \$self->{CTX}{BODY};
431              
432 2         9 my $template = "view/$self->{CTX}{LANG}/splash.tt";
433              
434 2 50       22 $self->{template}->process( $template, $self->{CTX}{TMPL_VARS}, $output )
435             or LOGDIE $self->{template}->error . "\n";
436             }
437              
438             ##############################################
439             # CLIENT API: client session automatically reactivated by matching
440             # IP/MAC tuple and cookie, show active page with
441             # proper informational message
442             #
443             sub idle_view {
444 0     0 0 0 my $self = shift;
445              
446 0         0 DEBUG('running IDLE reactivation handler ...');
447              
448             #############
449             # stop client request if firewall rules aren't loaded
450 0 0       0 LOGDIE "Firewall rules for Captive::Portal not loaded, "
451             . "please inform the administrators.\n"
452             unless defined $self->fw_status;
453              
454             # this requests parameters are in the context slot
455 0         0 my $query = $self->{CTX}{QUERY};
456 0         0 my $session = $self->{CTX}{SESSION};
457              
458 0         0 my $username = $session->{USERNAME};
459 0         0 my $ip = $session->{IP};
460 0         0 my $mac = $session->{MAC};
461              
462 0         0 $session->{STATE} = 'active';
463 0         0 $session->{STOP_TIME} = '';
464              
465             # EXCL lock, change ipset and session in one transaction
466             {
467 0         0 my $lock_handle = $self->get_session_lock_handle(
  0         0  
468             key => $ip,
469             shared => 0,
470             blocking => 1,
471             timeout => 3_000_000, # 3_000_000 us = 3s
472             );
473              
474             # remove possible ipset-entry due to some race condition
475 0     0   0 try { $self->fw_stop_session($ip) } catch {};
  0         0  
  0         0  
476              
477 0         0 $self->fw_start_session( $ip, $mac );
478 0         0 $self->write_session_handle( $lock_handle, $session );
479             }
480              
481 0         0 INFO "$username/$ip/$mac -> cookie match, session reactivated";
482              
483             # it's a reload after a idle session, reenabled with valid cookie
484 0         0 $self->{CTX}{TMPL_VARS}{msg_type} = 'info';
485 0         0 $self->{CTX}{TMPL_VARS}{msg_text} = $self->gettext('msg_006');
486              
487 0         0 return $self->active_view($session);
488             }
489              
490             ##############################################
491             # CLIENT API: show active page after login or reactivation
492             # after idle
493             #
494             sub active_view {
495 1     1 0 4 my $self = shift;
496              
497             # this requests parameters are in the context slot
498 1         4 my $query = $self->{CTX}{QUERY};
499 1         5 my $session = $self->{CTX}{SESSION};
500              
501 1         5 DEBUG('running ACTIVE handler ...');
502              
503 1         10 my $output = \$self->{CTX}{BODY};
504 1         6 $self->{CTX}{TMPL_VARS}{username} = $session->{USERNAME};
505              
506 1         4 my $template = "view/$self->{CTX}{LANG}/active.tt";
507              
508 1 50       13 $self->{template}->process( $template, $self->{CTX}{TMPL_VARS}, $output )
509             or LOGDIE $self->{template}->error . "\n";
510              
511 1         181 DEBUG "create http header with session cookie";
512              
513 1         24 $self->{CTX}{HEADER} = $query->header(
514             -type => 'text/html',
515             -charset => 'UTF-8',
516             -cookie => $self->mk_cookie,
517             );
518              
519             }
520              
521             ##############################################
522             # CLIENT API: process login and show active page
523             #
524             sub login {
525 1     1 0 74 my $self = shift;
526              
527 1         5 DEBUG('running LOGIN handler ...');
528              
529             # this requests parameters are in the context slot
530 1         9 my $query = $self->{CTX}{QUERY};
531 1         4 my $session = $self->{CTX}{SESSION};
532              
533 1         3 my $ip = $session->{IP};
534 1         3 my $mac = $session->{MAC};
535 1   50     17 my $user_agent = $query->user_agent || 'unknown';
536              
537 1         325 DEBUG("login requested for '$ip/$mac'");
538              
539 1 50       12 if ( $session->{STATE} eq 'active' ) {
540              
541             # STATE already active but login requested again,
542             # reset wrong url query params with external redirect
543 0         0 DEBUG('--> REDIRECT, login requested for ACTIVE session');
544              
545 0         0 $self->{CTX}{HEADER} = $query->redirect( $query->url );
546 0         0 return;
547             }
548              
549 1         7 my $username = lc $query->param('username');
550 1         37 my $password = $query->param('password');
551              
552             # forbid HTML code injection
553 1 50       30 $username = $query->escapeHTML($username) if $username;
554              
555 1 50 33     638 unless ( $username && $password ) {
556 0         0 DEBUG('parameter missing at login request');
557              
558 0         0 $self->{CTX}{TMPL_VARS}{username} = $username;
559 0         0 $self->{CTX}{TMPL_VARS}{msg_text} = $self->gettext('msg_002');
560 0         0 $self->{CTX}{TMPL_VARS}{msg_type} = 'error';
561              
562 0         0 return $self->splash_view;
563             }
564              
565             # trim whitespace
566 1         5 $username =~ s/^\s+|\s+$//g;
567 1         5 $password =~ s/^\s+|\s+$//g;
568              
569 1 50       9 unless ( $self->authenticate( $username, $password ) ) {
570 0         0 DEBUG("login FAILED for '$username'");
571              
572 0         0 $self->{CTX}{TMPL_VARS}{username} = $username;
573 0         0 $self->{CTX}{TMPL_VARS}{msg_text} = $self->gettext('msg_003');
574 0         0 $self->{CTX}{TMPL_VARS}{msg_type} = 'error';
575              
576 0         0 return $self->splash_view;
577             }
578              
579 1         10 DEBUG("login OK for '$username'");
580              
581             #############
582             # stop client request if firewall rules aren't loaded
583 1 50       13 LOGDIE "Firewall rules for Captive::Portal not loaded, "
584             . "please inform the administrators.\n"
585             unless defined $self->fw_status;
586              
587 1         4 $session->{STATE} = 'active';
588 1         4 $session->{START_TIME} = time();
589 1         3 $session->{STOP_TIME} = '';
590 1         5 $session->{USERNAME} = $username;
591 1         4 $session->{USER_AGENT} = $user_agent;
592 1         5 $session->{COOKIE} = $self->mk_cookie->value;
593              
594             # EXCL lock, change ipset and session in one transaction
595             {
596 1         24 my $lock_handle = $self->get_session_lock_handle(
  1         9  
597             key => $ip,
598             shared => 0,
599             blocking => 1,
600             timeout => 3_000_000, # 3_000_000 us = 3s
601             );
602              
603             # remove possible ipset-entry due to some race condition
604 1     1   12 try { $self->fw_stop_session($ip) } catch {};
  1         33  
  0         0  
605              
606 1         22 $self->fw_start_session( $ip, $mac );
607 1         9 $self->write_session_handle( $lock_handle, $session );
608             }
609              
610 1         95 INFO "$username/$ip/$mac -> login, User-Agent: $user_agent";
611              
612 1         16 return $self->active_view($session);
613             }
614              
615             ##############################################
616             # CLIENT API: process logout and show splash page
617             #
618             sub logout {
619 1     1 0 92 my $self = shift;
620              
621 1         6 DEBUG('running LOGOUT handler ...');
622              
623             # this requests parameters are in the context slot
624 1         10 my $query = $self->{CTX}{QUERY};
625 1         3 my $session = $self->{CTX}{SESSION};
626 1         3 my $ip = $session->{IP};
627 1         4 my $mac = $session->{MAC};
628              
629 1         8 DEBUG("logout requested for '$ip/$mac'");
630 1 50       11 unless ( $session->{STATE} eq 'active' ) {
631              
632             # no active session, but logout requested
633             # reset wrong url query params with external redirect
634 0         0 DEBUG('--> REDIRECT, logout requested for INACTIVE session');
635              
636 0         0 $self->{CTX}{HEADER} = $query->redirect( $query->url );
637 0         0 return;
638             }
639              
640             #############
641             # stop client request if firewall rules aren't loaded
642 1 50       10 LOGDIE "Firewall rules for Captive::Portal not loaded, "
643             . "please inform the administrators.\n"
644             unless defined $self->fw_status;
645              
646 1         4 $session->{STATE} = 'logout';
647 1         3 $session->{STOP_TIME} = time();
648 1         3 $session->{COOKIE} = undef;
649              
650 1         4 my $username = $session->{USERNAME};
651              
652             # EXCL lock, change ipset and session in one transaction
653             {
654 1         3 my $lock_handle = $self->get_session_lock_handle(
  1         6  
655             key => $ip,
656             shared => 0,
657             blocking => 1,
658             timeout => 3_000_000, # 3_000_000 us = 3s
659             );
660              
661 1         8 $self->write_session_handle( $lock_handle, $session );
662 1         12 $self->fw_stop_session($ip);
663             }
664              
665 1         84 INFO "$username/$ip/$mac -> logout";
666              
667 1         13 $self->{CTX}{TMPL_VARS}{username} = $username;
668 1         9 $self->{CTX}{TMPL_VARS}{msg_text} = $self->gettext('msg_004');
669 1         5 $self->{CTX}{TMPL_VARS}{msg_type} = 'info';
670              
671 1         7 return $self->splash_view;
672             }
673              
674             ##############################################
675             # ADMIN API: show brief status page
676             # if a matching admin secret is present, show
677             # a detail status page
678             #
679             sub summary_status_view {
680 3     3 0 9 my $self = shift;
681              
682             # this requests parameters are in the context slot
683 3         10 my $query = $self->{CTX}{QUERY};
684              
685 3         11 DEBUG('running SUMMARY_STATUS handler ...');
686              
687             # show detail_status, if cgi-param admin_secret exists
688 3 100       98 if ( exists $query->Vars->{admin_secret} ) {
689              
690 2 50       244 LOGDIE "ADMIN_SECRET missing in config file\n"
691             unless $self->cfg->{ADMIN_SECRET};
692              
693 2 50       18 if ( $query->param('admin_secret') eq $self->cfg->{ADMIN_SECRET} ) {
694 2         84 return $self->detail_status_view;
695             }
696             else {
697              
698 0         0 ERROR "wrong 'admin_secret'";
699              
700 0         0 $self->{CTX}{TMPL_VARS}{msg_text} = $self->gettext('msg_005');
701 0         0 $self->{CTX}{TMPL_VARS}{msg_type} = 'error';
702             }
703             }
704              
705 1         107 my $summary = {};
706              
707             # record session states
708 1         8 foreach my $key ( $self->list_sessions_from_disk ) {
709              
710             # fetch session data
711              
712 1         2 my ( $error, $lock_handle );
713             try {
714 1     1   42 $lock_handle = $self->get_session_lock_handle(
715             key => $key,
716             shared => 1,
717             blocking => 0,
718             try => 2,
719             );
720             }
721 1     0   15 catch { $error = $_ };
  0         0  
722              
723 1 50       20 if ($error) {
724 0         0 WARN "Couldn't get the lock for $key";
725 0         0 next;
726             }
727              
728 1         6 my $session = $self->read_session_handle($lock_handle);
729              
730 1 50       6 unless ($session) {
731              
732             # maybe just redirected, but no other action
733             # get_session_lock_handle creates emtpy session files
734 0         0 $summary->{init}++;
735              
736 0         0 next;
737             }
738              
739             # sum up the different session states
740 1         10 $summary->{ $session->{STATE} }++;
741              
742             }
743              
744 1 50       37 $self->{CTX}{TMPL_VARS}{stopped}++
745             unless defined $self->fw_status;
746              
747             # record seen active clients behind capo firewall
748 1         6 $self->{CTX}{TMPL_VARS}{client_candidates} =
749 1         7 scalar keys %{ $self->fw_list_activity };
750              
751 1         8 $self->{CTX}{TMPL_VARS}{query} = $query;
752 1         6 $self->{CTX}{TMPL_VARS}{summary} = $summary;
753              
754 1         4 my $output = \$self->{CTX}{BODY};
755 1         6 my $template = "view/$self->{CTX}{LANG}/summary_status.tt";
756              
757 1 50       10 $self->{template}->process( $template, $self->{CTX}{TMPL_VARS}, $output )
758             or LOGDIE $self->{template}->error . "\n";
759              
760 1         908 return;
761             }
762              
763             ##############################################
764             # ADMIN API: show detail status page
765             #
766             sub detail_status_view {
767 2     2 0 5 my $self = shift;
768              
769             # this requests parameters are in the context slot
770 2         8 my $query = $self->{CTX}{QUERY};
771              
772 2         11 DEBUG('running DETAIL_STATUS handler ...');
773              
774             # allowed query filter
775 2   50     21 my $filter_by_state = $query->param('filter_state') || undef;
776 2   50     46 my $filter_by_ip = $query->param('filter_ip') || undef;
777 2   50     50 my $filter_by_username = $query->param('filter_username') || undef;
778              
779 2         41 my @filtered_sessions = ();
780 2         6 my $summary = {};
781              
782 2         14 foreach my $key ( $self->list_sessions_from_disk ) {
783              
784             # fetch session data
785              
786 2         16 my $lock_handle = $self->get_session_lock_handle(
787             key => $key,
788             blocking => 1,
789             shared => 1,
790             timeout => 1_000_000, # 1_000_000 us = 1s
791             );
792              
793 2         13 my $session = $self->read_session_handle($lock_handle);
794              
795 2 50       7 unless ($session) {
796              
797             # maybe just redirected, but no other action
798             # get_session_lock_handle creates emtpy session files
799 0         0 $summary->{init}++;
800              
801 0         0 next;
802             }
803              
804             # sum up the different session states
805 2         10 $summary->{ $session->{STATE} }++;
806              
807 2 50       7 if ( defined $filter_by_state ) {
808             next
809 0 0       0 unless $session->{STATE} =~ m/\Q$filter_by_state\E/i;
810             }
811              
812 2 50       6 if ( defined $filter_by_ip ) {
813             next
814 0 0       0 unless $session->{IP} =~ m/\Q$filter_by_ip\E/i;
815             }
816              
817 2 50       6 if ( defined $filter_by_username ) {
818             next
819 0 0       0 unless $session->{USERNAME} =~ m/\Q$filter_by_username\E/i;
820             }
821              
822             # time() -> strftime() conversion for output
823              
824 2         7 my $start_time = $session->{START_TIME};
825 2         5 my $stop_time = $session->{STOP_TIME};
826              
827 2 50       274 $session->{LOCAL_START_TIME} =
828             $start_time
829             ? strftime( '%F %T', localtime($start_time) )
830             : '';
831              
832 2 50       11 $session->{LOCAL_STOP_TIME} =
833             $stop_time
834             ? strftime( '%F %T', localtime($stop_time) )
835             : '';
836              
837 2         18 $session->{IP_HEX} = $self->ip2hex( $session->{IP} );
838              
839 2         15 push @filtered_sessions, $session;
840             }
841              
842             ########################
843             # check sort params
844              
845 2         483 my $sort_reverse;
846 2 50       12 if ( $query->param('flip_sort_order') ) {
847 0         0 $query->delete('flip_sort_order');
848 0         0 undef $sort_reverse;
849             }
850             else {
851 2         210 $query->param( 'flip_sort_order', 1 );
852 2         141 $sort_reverse = 1;
853             }
854              
855 2 50       14 DEBUG "sort direction is reverse" if $sort_reverse;
856              
857 2         17 my $sort_by;
858 2 50       8 $sort_by = 'IP' if defined $query->param('sort_by_ip');
859 2 50       39 $sort_by = 'MAC' if defined $query->param('sort_by_mac');
860 2 50       36 $sort_by = 'USERNAME' if defined $query->param('sort_by_username');
861 2 50       35 $sort_by = 'STATE' if defined $query->param('sort_by_state');
862 2 50       35 $sort_by = 'START_TIME' if defined $query->param('sort_by_start_time');
863 2 50       36 $sort_by = 'STOP_TIME' if defined $query->param('sort_by_stop_time');
864              
865             # default
866 2   50     42 $sort_by ||= 'IP';
867              
868             # used for default string sort even for ip addresses and times
869 2 50       8 $sort_by = 'IP_HEX' if $sort_by eq 'IP';
870 2 50       7 $sort_by = 'LOCAL_START_TIME' if $sort_by eq 'START_TIME';
871 2 50       6 $sort_by = 'LOCAL_STOP_TIME' if $sort_by eq 'STOP_TIME';
872              
873 2         13 DEBUG "sort_by is set to '$sort_by'";
874              
875 2 50       17 if ($sort_reverse) {
876 0         0 @filtered_sessions =
877 2         5 sort { $b->{$sort_by} cmp $a->{$sort_by} } @filtered_sessions;
878             }
879             else {
880 0         0 @filtered_sessions =
881 0         0 sort { $a->{$sort_by} cmp $b->{$sort_by} } @filtered_sessions;
882             }
883              
884 2 50       17 $self->{CTX}{TMPL_VARS}{stopped}++
885             unless defined $self->fw_status;
886              
887             # record seen active clients behind capo firewall
888 2         11 $self->{CTX}{TMPL_VARS}{client_candidates} =
889 2         5 scalar keys %{ $self->fw_list_activity };
890              
891 2         17 $self->{CTX}{TMPL_VARS}{query} = $query;
892 2         8 $self->{CTX}{TMPL_VARS}{summary} = $summary;
893 2         7 $self->{CTX}{TMPL_VARS}{sessions} = \@filtered_sessions;
894              
895 2         6 my $output = \$self->{CTX}{BODY};
896              
897             # CGI parameter 'astext' defines html or text
898 2 100       73 if ( exists $query->Vars->{astext} ) {
899 1         122 $self->{CTX}{HEADER} =
900             $query->header( -type => 'text/plain', -charset => 'UTF-8' );
901              
902 1         562 my $template = 'view/any/status_astext.tt';
903              
904 1 50       11 $self->{template}
905             ->process( $template, $self->{CTX}{TMPL_VARS}, $output )
906             or LOGDIE $self->{template}->error . "\n";
907             }
908             else {
909 1         90 my $template = "view/$self->{CTX}{LANG}/detail_status.tt";
910              
911 1 50       13 $self->{template}
912             ->process( $template, $self->{CTX}{TMPL_VARS}, $output )
913             or LOGDIE $self->{template}->error . "\n";
914             }
915              
916 2         439 return;
917             }
918              
919             ##############################################
920             # ADMIN API: show current active session number
921             #
922             sub is_running_view {
923 1     1 0 3 my $self = shift;
924              
925             # this requests parameters are in the context slot
926 1         4 my $query = $self->{CTX}{QUERY};
927              
928 1         4 DEBUG('running IS_RUNNING handler ...');
929              
930 1         33 $self->{CTX}{HEADER} =
931             $query->header( -type => 'text/plain', -charset => 'UTF-8' );
932              
933 1         514 my $session_count = $self->fw_status;
934              
935 1 50       5 if ( defined $session_count ) {
936 1         6 $self->{CTX}{BODY} = "RUNNING $session_count active sessions";
937             }
938             else {
939 0         0 $self->{CTX}{BODY} = "STOPPED";
940             }
941              
942 1         4 return;
943             }
944              
945             ##############################################
946             # low level error page without template system
947             # something died, maybe some modules missing etc.
948             #
949             sub error_page_500 {
950 0     0 0   my $error_msg = shift;
951              
952             # cut off ... 'at file line xxx'
953 0           $error_msg =~ s/\s+ at \s+ \S+ \s+ line \s+ \d+ .*//x;
954              
955 0           my $html = <<'EOF_500';
956            
957            
958            
959             Captive::Portal - Error 500
960            
972            
973            
974            
975            
976            

Error 500

977            
978            

Internal Server Error

979            
980            
981             __ERROR_MSG__
982            
983            
984             Powered by Captive::Portal
985            
986            
987            
988            
989             EOF_500
990              
991 0           $html =~ s/__ERROR_MSG__/$error_msg/m;
992 0           return $html;
993             }
994              
995             1;
996              
997             =head1 SEE ALSO
998              
999             L, L and L
1000              
1001             =head1 CREDITS
1002              
1003             Most of the good parts have been implemented by many creative discussion with my colleague Bernd Leibing.
1004              
1005             =head1 BUGS AND LIMITATIONS
1006              
1007             There are no known problems with this module.
1008              
1009             Please report any bugs or feature requests to
1010             C, or through the web interface at
1011             L.
1012             I will be notified, and then you'll automatically be notified of progress on
1013             your bug as I make changes.
1014              
1015             =head1 AUTHOR
1016              
1017             Karl Gaissmaier, C<< >>
1018              
1019             =head1 LICENSE AND COPYRIGHT
1020              
1021             Copyright 2010-2013 Karl Gaissmaier, all rights reserved.
1022              
1023             This distribution is free software; you can redistribute it and/or modify it
1024             under the terms of either:
1025              
1026             a) the GNU General Public License as published by the Free Software
1027             Foundation; either version 2, or (at your option) any later version, or
1028              
1029             b) the Artistic License version 2.0.
1030              
1031             =cut