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 |