line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Plack::App::OpenVPN::Status; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# ABSTRACT: Plack application to display the sessions of OpenVPN server |
4
|
|
|
|
|
|
|
|
5
|
3
|
|
|
3
|
|
357580
|
use strict; |
|
3
|
|
|
|
|
10
|
|
|
3
|
|
|
|
|
101
|
|
6
|
3
|
|
|
3
|
|
20
|
use warnings; |
|
3
|
|
|
|
|
8
|
|
|
3
|
|
|
|
|
94
|
|
7
|
3
|
|
|
3
|
|
18
|
use feature ':5.10'; |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
3148
|
|
8
|
|
|
|
|
|
|
|
9
|
3
|
|
|
3
|
|
17
|
use parent 'Plack::Component'; |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
17
|
|
10
|
3
|
|
|
3
|
|
145
|
use Carp (); |
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
44
|
|
11
|
3
|
|
|
3
|
|
4772
|
use Text::MicroTemplate; |
|
3
|
|
|
|
|
14641
|
|
|
3
|
|
|
|
|
158
|
|
12
|
3
|
|
|
3
|
|
21
|
use Plack::Util::Accessor qw/renderer status_from custom_view/; |
|
3
|
|
|
|
|
5
|
|
|
3
|
|
|
|
|
28
|
|
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
our $VERSION = '0.16'; # VERSION |
15
|
|
|
|
|
|
|
our $AUTHORITY = 'cpan:CHIM'; # AUTHORITY |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
# |
18
|
|
|
|
|
|
|
# default view (uses Twitter Bootstrap v2.x.x layout) |
19
|
|
|
|
|
|
|
sub default_view { |
20
|
8
|
|
|
8
|
1
|
20
|
<<'EOTMPL' } |
21
|
|
|
|
|
|
|
% my $vars = $_[0]; |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
OpenVPN Status |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
Active OpenVPN Sessions Updated <%= $vars->{updated} %> |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
Status Version #<%= $vars->{version} %> |
38
|
|
|
|
|
|
|
% if (scalar @{$vars->{users}}) { |
39
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
| Virtual address |
43
|
|
|
|
|
|
|
| Common name |
44
|
|
|
|
|
|
|
| Remote IP (port) |
45
|
|
|
|
|
|
|
| Recv (from) |
46
|
|
|
|
|
|
|
| Xmit (to) |
47
|
|
|
|
|
|
|
| Connected since |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
| |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
% for my $user (@{$vars->{users}}) { |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
| <%= $user->{'virtual'} %> |
54
|
|
|
|
|
|
|
| <%= $user->{'common-name'} %> |
55
|
|
|
|
|
|
|
| <%= $user->{'remote-ip'} %> (<%= $user->{'remote-port'} %>) |
56
|
|
|
|
|
|
|
| <%= $user->{'rx-bytes'} %> |
57
|
|
|
|
|
|
|
| <%= $user->{'tx-bytes'} %> |
58
|
|
|
|
|
|
|
| <%= $user->{'connected'} %> |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
% } |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
| |
63
|
|
|
|
|
|
|
% } else { |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
Attention! |
66
|
|
|
|
|
|
|
There is no connected OpenVPN users. |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
% } |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
EOTMPL |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
# |
76
|
|
|
|
|
|
|
# some preparations |
77
|
|
|
|
|
|
|
sub prepare_app { |
78
|
8
|
|
|
8
|
1
|
87355
|
my ($self) = @_; |
79
|
|
|
|
|
|
|
|
80
|
8
|
|
|
|
|
32
|
my $t_view = $self->default_view; |
81
|
|
|
|
|
|
|
|
82
|
8
|
100
|
|
|
|
39
|
if ($self->custom_view) { |
83
|
2
|
100
|
|
|
|
78
|
if (ref($self->custom_view) eq 'CODE') { |
84
|
1
|
|
|
|
|
8
|
$t_view = $self->custom_view->(); |
85
|
|
|
|
|
|
|
} |
86
|
|
|
|
|
|
|
else { |
87
|
1
|
|
|
|
|
233
|
Carp::croak "Parameter 'custom_view' must be a CODEREF"; |
88
|
|
|
|
|
|
|
} |
89
|
|
|
|
|
|
|
} |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
$self->renderer( |
92
|
7
|
|
|
|
|
104
|
Text::MicroTemplate->new( |
93
|
|
|
|
|
|
|
template => $t_view, |
94
|
|
|
|
|
|
|
tag_start => '<%', |
95
|
|
|
|
|
|
|
tag_end => '%>', |
96
|
|
|
|
|
|
|
line_start => '%', |
97
|
|
|
|
|
|
|
)->build |
98
|
|
|
|
|
|
|
); |
99
|
|
|
|
|
|
|
} |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
# |
102
|
|
|
|
|
|
|
# execute application |
103
|
|
|
|
|
|
|
sub call { |
104
|
7
|
|
|
7
|
1
|
55204
|
my ($self, $env) = @_; |
105
|
|
|
|
|
|
|
|
106
|
7
|
|
|
|
|
17
|
my ($body); |
107
|
|
|
|
|
|
|
|
108
|
7
|
100
|
|
|
|
36
|
unless ($self->status_from) { |
109
|
1
|
|
|
|
|
9
|
$body = "Error: OpenVPN status file is not set!"; |
110
|
|
|
|
|
|
|
} |
111
|
|
|
|
|
|
|
else { |
112
|
6
|
100
|
66
|
|
|
66
|
unless (-e $self->status_from || -r _) { |
113
|
1
|
|
|
|
|
36
|
$body = "Error: OpenVPN status file '" . $self->status_from . "' does not exist or unreadable!"; |
114
|
|
|
|
|
|
|
} |
115
|
|
|
|
|
|
|
else { |
116
|
5
|
|
|
|
|
183
|
$body = $self->renderer->($self->openvpn_status); |
117
|
|
|
|
|
|
|
} |
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
|
120
|
7
|
|
|
|
|
682
|
[ 200, [ 'Content-Type' => 'text/html; charset=utf-8' ], [ $body ] ]; |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
# |
124
|
|
|
|
|
|
|
# parse OpenVPN status log |
125
|
|
|
|
|
|
|
sub openvpn_status { |
126
|
5
|
|
|
5
|
1
|
11
|
my ($self) = @_; |
127
|
|
|
|
|
|
|
|
128
|
5
|
|
|
|
|
12
|
my $lines; |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
{ |
131
|
5
|
|
|
|
|
8
|
local $/ = undef; |
|
5
|
|
|
|
|
27
|
|
132
|
5
|
50
|
|
|
|
21
|
open STATUS, '<' . $self->status_from or Carp::croak "Cannot open '" . $self->status_from . "'"; |
133
|
5
|
|
|
|
|
334
|
$lines = ; |
134
|
5
|
|
|
|
|
94
|
close STATUS; |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
5
|
|
|
|
|
12
|
my ($st_ver, $delim, $sub); |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
# guess status file version |
140
|
5
|
|
|
|
|
11
|
given ($lines) { |
141
|
5
|
|
|
|
|
22
|
when (/TITLE,/) { |
142
|
1
|
|
|
|
|
3
|
$st_ver = 2; |
143
|
1
|
|
|
|
|
2
|
$delim = ','; |
144
|
1
|
|
|
|
|
4
|
$sub = \&_ovpn_status_v2_parse; |
145
|
|
|
|
|
|
|
} |
146
|
4
|
|
|
|
|
13
|
when (/TITLE\t/) { |
147
|
1
|
|
|
|
|
2
|
$st_ver = 3; |
148
|
1
|
|
|
|
|
3
|
$delim = '\t'; |
149
|
1
|
|
|
|
|
4
|
$sub = \&_ovpn_status_v2_parse; |
150
|
|
|
|
|
|
|
} |
151
|
3
|
|
|
|
|
8
|
default { |
152
|
3
|
|
|
|
|
6
|
$st_ver = 1; |
153
|
3
|
|
|
|
|
10
|
$delim = ','; |
154
|
3
|
|
|
|
|
12
|
$sub = \&_ovpn_status_v1_parse; |
155
|
|
|
|
|
|
|
} |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
|
158
|
5
|
|
|
|
|
22
|
$sub->($lines, $delim, $st_ver); |
159
|
|
|
|
|
|
|
} |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
# octets formatter |
162
|
|
|
|
|
|
|
# http://en.wikipedia.org/wiki/Octet_%28computing%29 |
163
|
|
|
|
|
|
|
sub _adaptive_octets { |
164
|
8
|
|
|
8
|
|
14
|
my ($octets) = @_; |
165
|
|
|
|
|
|
|
|
166
|
8
|
50
|
|
|
|
62
|
if ($octets > 1152921504606846976) { # exbioctet (Eio) = 2^60 octets |
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
167
|
0
|
|
|
|
|
0
|
$octets = sprintf('%.6f Eio', $octets/1152921504606846976); |
168
|
|
|
|
|
|
|
} |
169
|
|
|
|
|
|
|
elsif ($octets > 1125899906842624) { # pebioctet (Pio) = 2^50 octets |
170
|
0
|
|
|
|
|
0
|
$octets = sprintf('%.5f Pio', $octets/1125899906842624); |
171
|
|
|
|
|
|
|
} |
172
|
|
|
|
|
|
|
elsif ($octets > 1099511627776) { # tebioctet (Tio) = 2^40 octets |
173
|
0
|
|
|
|
|
0
|
$octets = sprintf('%.4f Tio', $octets/1099511627776); |
174
|
|
|
|
|
|
|
} |
175
|
|
|
|
|
|
|
elsif ($octets > 1073741824) { # gibioctet (Gio) = 2^30 octets |
176
|
0
|
|
|
|
|
0
|
$octets = sprintf('%.3f Gio', $octets/1073741824); |
177
|
|
|
|
|
|
|
} |
178
|
|
|
|
|
|
|
elsif ($octets > 1048576) { # mebioctet (Mio) = 2^20 octets |
179
|
7
|
|
|
|
|
62
|
$octets = sprintf('%.2f Mio', $octets/1048576); |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
elsif ($octets > 1024) { # kibioctet (Kio) = 2^10 octets |
182
|
1
|
|
|
|
|
14
|
$octets = sprintf('%.1f Kio', $octets/1024); |
183
|
|
|
|
|
|
|
} |
184
|
|
|
|
|
|
|
|
185
|
8
|
|
|
|
|
71
|
$octets; |
186
|
|
|
|
|
|
|
}; |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
# |
189
|
|
|
|
|
|
|
# OpenVPN status file format version #1 parser |
190
|
|
|
|
|
|
|
sub _ovpn_status_v1_parse { |
191
|
3
|
|
|
3
|
|
8
|
my ($lines, $delim, $version) = @_; |
192
|
|
|
|
|
|
|
|
193
|
3
|
|
|
|
|
7
|
my $vars = {}; |
194
|
|
|
|
|
|
|
|
195
|
3
|
|
|
|
|
7
|
my ($users, $updated); |
196
|
|
|
|
|
|
|
|
197
|
3
|
|
|
|
|
23
|
for (split /\n/, $lines) { |
198
|
28
|
50
|
|
|
|
81
|
next if /^$/; |
199
|
28
|
100
|
|
|
|
128
|
next if /^(OpenVPN|ROUTING TABLE|GLOBAL STATS|Max bcast|END)/; |
200
|
|
|
|
|
|
|
|
201
|
13
|
|
|
|
|
99
|
my @line = split $delim, $_; |
202
|
13
|
|
|
|
|
24
|
my $length = scalar(@line); |
203
|
|
|
|
|
|
|
|
204
|
13
|
100
|
|
|
|
52
|
$length == 2 && do { |
205
|
3
|
50
|
|
|
|
18
|
next unless $line[0] =~ /^Updated/; |
206
|
3
|
|
|
|
|
5
|
$updated = $line[1]; |
207
|
3
|
|
|
|
|
9
|
next; |
208
|
|
|
|
|
|
|
}; |
209
|
|
|
|
|
|
|
|
210
|
10
|
100
|
|
|
|
26
|
$length == 5 && do { |
211
|
5
|
100
|
|
|
|
25
|
next if $line[0] =~ /^Common Name/; |
212
|
2
|
|
|
|
|
7
|
my ($ip, $port) = split /:/, $line[1]; |
213
|
2
|
|
|
|
|
12
|
$users->{$line[0]} = { |
214
|
|
|
|
|
|
|
'common-name' => $line[0], |
215
|
|
|
|
|
|
|
'remote-ip' => $ip, |
216
|
|
|
|
|
|
|
'remote-port' => $port, |
217
|
|
|
|
|
|
|
'rx-bytes' => _adaptive_octets($line[2]), |
218
|
|
|
|
|
|
|
'tx-bytes' => _adaptive_octets($line[3]), |
219
|
|
|
|
|
|
|
'connected' => $line[4], |
220
|
|
|
|
|
|
|
}; |
221
|
2
|
|
|
|
|
9
|
next; |
222
|
|
|
|
|
|
|
}; |
223
|
|
|
|
|
|
|
|
224
|
5
|
50
|
|
|
|
17
|
$length == 4 && do { |
225
|
5
|
100
|
|
|
|
26
|
next if $line[0] =~ /^Virtual Address/; |
226
|
2
|
|
|
|
|
6
|
$users->{$line[1]}->{'virtual'} = $line[0]; |
227
|
2
|
|
|
|
|
7
|
$users->{$line[1]}->{'last-ref'} = $line[3]; |
228
|
2
|
|
|
|
|
11
|
next; |
229
|
|
|
|
|
|
|
}; |
230
|
|
|
|
|
|
|
} |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
$vars = { |
233
|
2
|
|
|
|
|
13
|
'version' => $version, |
234
|
|
|
|
|
|
|
'updated' => $updated, |
235
|
3
|
|
|
|
|
24
|
'users' => [ map { $users->{$_} } keys %$users ], |
236
|
|
|
|
|
|
|
}; |
237
|
|
|
|
|
|
|
|
238
|
3
|
|
|
|
|
24
|
$vars; |
239
|
|
|
|
|
|
|
} |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
# |
242
|
|
|
|
|
|
|
# OpenVPN status file format version #2 and #3 parser |
243
|
|
|
|
|
|
|
sub _ovpn_status_v2_parse { |
244
|
2
|
|
|
2
|
|
5
|
my ($lines, $delim, $version) = @_; |
245
|
|
|
|
|
|
|
|
246
|
2
|
|
|
|
|
5
|
my $vars = {}; |
247
|
|
|
|
|
|
|
|
248
|
2
|
|
|
|
|
3
|
my ($users, $updated); |
249
|
|
|
|
|
|
|
|
250
|
2
|
|
|
|
|
17
|
for (split /\n/, $lines) { |
251
|
16
|
50
|
|
|
|
41
|
next if /^$/; |
252
|
16
|
100
|
|
|
|
50
|
next if /^(TITLE|HEADER|GLOBAL_STATS|END)/; |
253
|
|
|
|
|
|
|
|
254
|
6
|
|
|
|
|
45
|
my @line = split $delim, $_; |
255
|
6
|
|
|
|
|
11
|
my $length = scalar(@line); |
256
|
|
|
|
|
|
|
|
257
|
6
|
100
|
|
|
|
16
|
$length == 3 && do { |
258
|
2
|
50
|
|
|
|
9
|
next unless $line[0] =~ /^TIME/; |
259
|
2
|
|
|
|
|
4
|
$updated = $line[1]; |
260
|
2
|
|
|
|
|
6
|
next; |
261
|
|
|
|
|
|
|
}; |
262
|
|
|
|
|
|
|
|
263
|
4
|
100
|
|
|
|
10
|
$length == 8 && do { |
264
|
2
|
50
|
|
|
|
9
|
next unless $line[0] =~ /^CLIENT_LIST/; |
265
|
2
|
|
|
|
|
8
|
my ($ip, $port) = split /:/, $line[2]; |
266
|
2
|
|
|
|
|
11
|
$users->{$line[1]} = { |
267
|
|
|
|
|
|
|
'common-name' => $line[1], |
268
|
|
|
|
|
|
|
'remote-ip' => $ip, |
269
|
|
|
|
|
|
|
'remote-port' => $port, |
270
|
|
|
|
|
|
|
'rx-bytes' => _adaptive_octets($line[4]), |
271
|
|
|
|
|
|
|
'tx-bytes' => _adaptive_octets($line[5]), |
272
|
|
|
|
|
|
|
'connected' => $line[6], |
273
|
|
|
|
|
|
|
}; |
274
|
2
|
|
|
|
|
7
|
next; |
275
|
|
|
|
|
|
|
}; |
276
|
|
|
|
|
|
|
|
277
|
2
|
50
|
|
|
|
8
|
$length == 6 && do { |
278
|
2
|
50
|
|
|
|
9
|
next unless $line[0] =~ /^ROUTING_TABLE/; |
279
|
2
|
|
|
|
|
5
|
$users->{$line[2]}->{'virtual'} = $line[1]; |
280
|
2
|
|
|
|
|
7
|
$users->{$line[2]}->{'last-ref'} = $line[4]; |
281
|
2
|
|
|
|
|
5
|
next; |
282
|
|
|
|
|
|
|
}; |
283
|
|
|
|
|
|
|
} |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
$vars = { |
286
|
2
|
|
|
|
|
10
|
'version' => $version, |
287
|
|
|
|
|
|
|
'updated' => $updated, |
288
|
2
|
|
|
|
|
9
|
'users' => [ map { $users->{$_} } keys %$users ], |
289
|
|
|
|
|
|
|
}; |
290
|
|
|
|
|
|
|
|
291
|
2
|
|
|
|
|
12
|
$vars; |
292
|
|
|
|
|
|
|
} |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
1; # End of Plack::App::OpenVPN::Status |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
__END__ |