File Coverage

blib/lib/Apache/Quota.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Apache::Quota;
2              
3 1     1   847 use strict;
  1         1  
  1         34  
4              
5 1     1   387 use Apache;
  0            
  0            
6             use Apache::Constants qw( OK DECLINED FORBIDDEN HTTP_OK );
7             use Fcntl qw(O_CREAT O_RDWR O_RDONLY);
8             use File::Path ();
9             use File::Temp ();
10              
11             use vars qw ( $VERSION $DEBUG );
12              
13             $VERSION = 0.04;
14              
15             use constant KB => 1024;
16             use constant MB => 1024 ** 2;
17             use constant GB => 1024 ** 3;
18              
19             use constant SECOND => 1;
20             use constant MINUTE => 60;
21             use constant HOUR => 60 * 60;
22             use constant DAY => 24 * 60 * 60;
23              
24             sub handler
25             {
26             my $r = shift;
27              
28             return DECLINED if $r->header_only;
29              
30             local $DEBUG = $r->dir_config('QuotaDebug') ? 1 : 0;
31             $r->notes( 'Apache::Quota::DEBUG' => 1 ) if $DEBUG;
32              
33             my $main = $r->is_main ? $r : $r->main;
34              
35             unless ( $main->notes('Apache::Quota::initialized') )
36             {
37             my $file = $r->dir_config('QuotaFile');
38              
39             die "Cannot use Apache::Quota unless QuotaFile is specified"
40             unless defined $file;
41              
42             $main->notes( 'Apache::Quota::file' => $file );
43              
44             my $locker;
45             {
46             my $class;
47              
48             if ( $r->dir_config('QuotaLocker') )
49             {
50             $class = $r->dir_config('QuotaLocker');
51             $class = "Apache::Quota::$class"
52             unless $class =~ /^Apache::Quota::/;
53              
54             eval "require $class";
55             die $@ if $@;
56             }
57             else
58             {
59             for my $c ( qw( Apache::Quota::BerkeleyDB Apache::Quota::DB_File::Lock ) )
60             {
61             eval "require $_";
62             $class = $c unless $@;
63             }
64              
65             die "Cannot load Apache::Quota::BerkeleyDB or Apache::Quota::DB_File::Lock"
66             unless $class;
67             }
68              
69             _log( $r, "locker is $class" ) if $DEBUG;
70              
71             $locker = $class;
72             $main->notes( 'Apache::Quota::locker' => $locker );
73             }
74              
75              
76             _log( $r, "will record usage in $file for request for " . $r->uri )
77             if $DEBUG;
78              
79             my $type = lc $r->dir_config('QuotaType') || 'global';
80              
81             _log( $r, "limit type is $type" )
82             if $DEBUG;
83              
84             my $key = $r->dir_config('QuotaLocationKey') || 'Apache-Quota global key';
85              
86             _log( $r, "key is $key" )
87             if $DEBUG;
88              
89             die "Your location key cannot contain a colon" if $key =~ /:/;
90              
91             my $full_key = ( $type eq 'client-ip'
92             ? join ':', 'client-ip', $r->connection->remote_ip, $key
93             : $type eq 'sub'
94             ? join ':', 'sub', _key_from_sub($r), $key
95             : $key
96             );
97              
98             _log( $r, "full key is $full_key" )
99             if $DEBUG;
100              
101             $main->notes( 'Apache::Quota::full_key' => $full_key );
102              
103             my $limit = _get_limit($r);
104              
105             _log( $r, "limit is $limit bytes" )
106             if $DEBUG;
107              
108             my $period = _get_period($r);
109              
110             _log( $r, "period is $period seconds" )
111             if $DEBUG;
112              
113             my $db = $locker->_open_db( file => $file, mode => 'read' );
114              
115             my $expired = time - $period;
116             $main->notes( 'Apache::Quota::expired' => $expired );
117              
118             if ( exists $db->{$full_key} )
119             {
120             _log( $r, "found key ($full_key) in DB file" )
121             if $DEBUG;
122              
123             $main->notes( 'Apache::Quota::exceeds' =>
124             _check_against_limit( $r, $expired, $limit, $db->{$full_key} )
125             );
126             }
127              
128             untie %$db;
129             }
130              
131             $main->notes( 'Apache::Quota::initialized' => 1 );
132              
133             if ( $main->notes('Apache::Quota::exceeds') )
134             {
135             my $on_exceed = lc $r->dir_config('QuotaOnExceed') || 'deny';
136              
137             _log( $r, "on exceed is $on_exceed" )
138             if $DEBUG;
139              
140             if ( $on_exceed eq 'notes' )
141             {
142             _log( $r, "setting \$r->notes() value for Apache::Quota::exceeded to 1" )
143             if $DEBUG;
144              
145             $r->notes( 'Apache::Quota::exceeded' => 1 );
146             }
147             else
148             {
149             _log( $r, "returning FORBIDDEN" )
150             if $DEBUG;
151              
152             return FORBIDDEN;
153             }
154             }
155             else
156             {
157             _log( $r, "registering cleanup" )
158             if $DEBUG;
159              
160             $r->register_cleanup( \&_record_usage );
161             }
162              
163             return OK;
164             }
165              
166             sub _key_from_sub
167             {
168             my $r = shift;
169              
170             my $sub = $r->dir_config('QuotaSub');
171              
172             die "Cannot set QuotaType to sub if QuotaSub is not set"
173             unless $sub;
174              
175             _log( $r, "sub is $sub" )
176             if $DEBUG;
177              
178             my $key;
179             {
180             no strict 'refs';
181             $key = &{$sub}($r);
182             }
183              
184             return $key;
185             }
186              
187             my %limit_letter_to_number = ( k => KB, m => MB, g => GB );
188             sub _get_limit
189             {
190             my $r = shift;
191              
192             my $limit = $r->dir_config('QuotaLimit');
193              
194             die "Cannot use Apache::Quota unless QuotaLimit is specified"
195             unless defined $limit;
196              
197             _log( $r, "limit set to $limit in config file" )
198             if $DEBUG;
199              
200             my ( $num, $letter ) = $limit =~ /(\d+)([kmg])/i;
201             $letter ||= 'k';
202              
203             die "Invalid limit in QuotaLimit: $limit"
204             unless exists $limit_letter_to_number{lc $letter};
205              
206             return $num * $limit_letter_to_number{ lc $letter };
207             }
208              
209             sub _get_period
210             {
211             my $r = shift;
212              
213             my $period = $r->dir_config('QuotaPeriod');
214              
215             die "Cannot use Apache::Quota unless QuotaPeriod is specified"
216             unless defined $period;
217              
218             _log( $r, "period set to $period in config file" )
219             if $DEBUG;
220              
221             return _parse_period($period);
222             }
223              
224             my %period_letter_to_number = ( s => SECOND, m => MINUTE, h => HOUR, d => DAY );
225             sub _parse_period
226             {
227             my $period = shift;
228              
229             my ( $num, $letter ) = $period =~ /(\d+)([smhd])/i;
230             $letter ||= 's';
231              
232             die "Invalid period: $period"
233             unless exists $period_letter_to_number{ lc $letter };
234              
235             return $num * $period_letter_to_number{ lc $letter };
236             }
237              
238             sub _check_against_limit
239             {
240             my ( $r, $expired, $limit, $current ) = @_;
241              
242             my $total = 0;
243             foreach my $record ( split /;/, $current )
244             {
245             my ( $time, $bytes ) = split /:/, $record;
246              
247             _log( $r, "key has record: $time - $bytes" )
248             if $DEBUG;
249              
250             # These will be removed during cleanup, but for now we'll
251             # avoid writing to the file to try to speed things up a bit.
252             next unless $time > $expired;
253              
254             $total += $bytes;
255             }
256              
257             _log( $r, "key has total bytes of $total" )
258             if $DEBUG;
259              
260             if ( $total > $limit )
261             {
262             _log( $r, "total bytes ($total) exceeds limit ($limit)" )
263             if $DEBUG;
264              
265             return 1;
266             }
267              
268             return 0;
269             }
270              
271             sub _record_usage
272             {
273             my $r = shift;
274              
275             return 0 unless $r->status == HTTP_OK;
276              
277             return 0 unless $r->bytes_sent;
278              
279             _log( $r, "status was HTTP_OK, recording traffic" )
280             if $DEBUG;
281              
282             my $db =
283             $r->notes('Apache::Quota::locker')->_open_db
284             ( file => $r->notes('Apache::Quota::file'), mode => 'write' );
285              
286             my $key = $r->notes('Apache::Quota::full_key');
287             my $expired = $r->notes('Apache::Quota::expired');
288              
289             my @records;
290             if ( exists $db->{$key} )
291             {
292             foreach my $record ( split /;/, $db->{$key} )
293             {
294             my ( $time, $bytes ) = split /:/, $record;
295              
296             next unless $time > $expired;
297              
298             push @records, "$time:$bytes";
299             }
300             }
301              
302             push @records, join ':', time, $r->bytes_sent;
303              
304             _log( $r, "adding record of " . $r->bytes_sent . " bytes sent" )
305             if $r->notes('Apache::Quota::DEBUG');
306              
307             $db->{$key} = join ';', @records;
308              
309             untie %$db;
310              
311             return 1;
312             }
313              
314             sub usage
315             {
316             my %p = @_;
317              
318             my $locker = $p{locker};
319             $locker = "Apache::Quota::$locker"
320             unless $locker =~ /^Apache::Quota::/;
321              
322             eval "require $locker";
323             die $@ if $@;
324              
325             my $db = $locker->_open_db( file => $p{file}, mode => 'read' );
326              
327             my $expired = 0;
328             if ( defined $p{period} )
329             {
330             $expired = time - _parse_period( $p{period} );
331             }
332              
333             my %vals;
334             foreach my $key ( keys %$db )
335             {
336             my $total = 0;
337             if ( defined $db->{$key} )
338             {
339             foreach my $record ( split /;/, $db->{$key} )
340             {
341             my ( $time, $bytes ) = split /:/, $record;
342              
343             next unless $time > $expired;
344              
345             $total += $bytes;
346             }
347             }
348              
349             next unless $total;
350              
351             my %extra;
352             if ( $key =~ /^client-ip:([\d\.]+)/ )
353             {
354             $extra{ip} = $1;
355             }
356             elsif ( $key =~ /^sub:(.+):[^:]+$/ )
357             {
358             $extra{sub} = $1;
359             }
360              
361             $vals{$key} = { %extra, bytes => $total };
362             }
363              
364             untie %$db;
365              
366             return %vals;
367             }
368              
369             sub set_key
370             {
371             my %p = @_;
372              
373             die "Cannot call set_key without a key parameter"
374             unless defined $p{key};
375              
376             my $locker = $p{locker};
377             $locker = "Apache::Quota::$locker"
378             unless $locker =~ /^Apache::Quota::/;
379              
380             eval "require $locker";
381             die $@ if $@;
382              
383             my $db = $locker->_open_db( file => $p{file}, mode => 'write' );
384              
385             return 0 unless exists $db->{ $p{key} };
386              
387             if ( defined $p{value} )
388             {
389             die "Cannot set quota to a non-numeric value"
390             unless $p{value} =~ /^\d+$/;
391              
392             $db->{ $p{key} } = time . ":$p{value}";
393             }
394             else
395             {
396             delete $db->{ $p{key} };
397             }
398              
399             untie %$db;
400              
401             return 1;
402             }
403              
404             sub reset_key { set_key( @_, value => undef ) }
405              
406             sub _log
407             {
408             my $r = shift;
409             my $msg = '[Apache::Quota debug] ' . shift;
410             $msg .= "\n";
411              
412             warn($msg);
413             }
414              
415             1;
416              
417             __END__