blib/lib/CGI/Application/Plugin/LinkIntegrity.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 128 | 133 | 96.2 |
branch | 34 | 42 | 80.9 |
condition | 20 | 27 | 74.0 |
subroutine | 18 | 18 | 100.0 |
pod | 4 | 4 | 100.0 |
total | 204 | 224 | 91.0 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | |||||||
2 | package CGI::Application::Plugin::LinkIntegrity; | ||||||
3 | |||||||
4 | 8 | 8 | 446869 | use warnings; | |||
8 | 22 | ||||||
8 | 289 | ||||||
5 | 8 | 8 | 44 | use strict; | |||
8 | 17 | ||||||
8 | 622 | ||||||
6 | |||||||
7 | =head1 NAME | ||||||
8 | |||||||
9 | CGI::Application::Plugin::LinkIntegrity - Make tamper-resisistent links in CGI::Application | ||||||
10 | |||||||
11 | =head1 VERSION | ||||||
12 | |||||||
13 | Version 0.06 | ||||||
14 | |||||||
15 | =cut | ||||||
16 | |||||||
17 | our $VERSION = '0.06'; | ||||||
18 | |||||||
19 | =head1 SYNOPSIS | ||||||
20 | |||||||
21 | In your application: | ||||||
22 | |||||||
23 | use base 'CGI::Application'; | ||||||
24 | use CGI::Application::Plugin::LinkIntegrity; | ||||||
25 | |||||||
26 | sub setup { | ||||||
27 | my $self = shift; | ||||||
28 | $self->link_integrity_config( | ||||||
29 | secret => 'some secret string known only to you and me', | ||||||
30 | ); | ||||||
31 | } | ||||||
32 | |||||||
33 | sub account_info { | ||||||
34 | my $self = shift; | ||||||
35 | |||||||
36 | my $account_id = get_user_account_id(); | ||||||
37 | |||||||
38 | my $template = $self->load_tmpl('account.html'); | ||||||
39 | |||||||
40 | $template->param( | ||||||
41 | 'balance' => $self->link("/account.cgi?rm=balance&acct_id=$account_id"); | ||||||
42 | 'transfer' => $self->link("/account.cgi?rm=transfer&acct_id=$account_id"); | ||||||
43 | 'withdrawal' => $self->link("/account.cgi?rm=withdrawl&acct_id=$account_id"); | ||||||
44 | ); | ||||||
45 | } | ||||||
46 | |||||||
47 | In your template: | ||||||
48 | |||||||
49 | Welcome to The Faceless Banking Corp. |
||||||
50 | Actions: |
||||||
51 | ">Show Balance |
||||||
52 | ">Make a Transfer |
||||||
53 | ">Get Cash |
||||||
54 | |||||||
55 | |||||||
56 | This will send the following HTML to the browser: | ||||||
57 | |||||||
58 | Welcome to The Faceless Banking Corp. |
||||||
59 | Actions: |
||||||
60 | Show Balance |
||||||
61 | Make a Transfer |
||||||
62 | Get Cash |
||||||
63 | |||||||
64 | The URLs created are now tamper-resistent. If the user changes | ||||||
65 | C |
||||||
66 | system will treat it as an intrusion attempt. | ||||||
67 | |||||||
68 | =head2 Calling link and self_link directly from the template | ||||||
69 | |||||||
70 | If you use C |
||||||
71 | C |
||||||
72 | C<$self> object into the template and call C and C |
||||||
73 | from the template. In your app: | ||||||
74 | |||||||
75 | $template->param( | ||||||
76 | 'app' => $self, | ||||||
77 | 'name' => 'gordon', | ||||||
78 | 'email' => 'gordon@example.com', | ||||||
79 | ); | ||||||
80 | |||||||
81 | And in your template you can use | ||||||
82 | |||||||
83 | # Template::Toolkit syntax | ||||||
84 | ... | ||||||
85 | |||||||
86 | # HTML::Template::Plugin::Dot syntax | ||||||
87 | ">... | ||||||
88 | |||||||
89 | # Petal syntax | ||||||
90 | |||||||
91 | tal:attributes="href app/self_link('name', name, 'email', email)">... | ||||||
92 | |||||||
93 | Note that in the parameters of the call to << link >>, items enclosed in | ||||||
94 | quotes are treated as literal parameters and barewords are treated as | ||||||
95 | template params. So C<'email'> is the literal string, and C |
||||||
96 | the template paramter named email (in this case 'gordon@example.com'). | ||||||
97 | |||||||
98 | =head1 DESCRIPTION | ||||||
99 | |||||||
100 | C |
||||||
101 | tamper-resistent links within your CGI::Application project. When you | ||||||
102 | create an URL with C, a C<_checksum> is added to the URL: | ||||||
103 | |||||||
104 | my $link = $self->link("/account.cgi?rm=balance&acct_id=73"); | ||||||
105 | print $link; # /account.cgi?rm=balance&acct_id=73&_checksum=1d7c4b82d075785de04fa6b98b572691 | ||||||
106 | |||||||
107 | The checksum is a (cryptographic) hash of the URL, plus a secret string | ||||||
108 | known only to the server. | ||||||
109 | |||||||
110 | If the user attempts to change part of the URL (e.g. a query string | ||||||
111 | parameter, or the PATH_INFO), then the checksum will not match. The run | ||||||
112 | mode will be changed to C |
||||||
113 | hook will be called. | ||||||
114 | |||||||
115 | You can define the C |
||||||
116 | the default C |
||||||
117 | L |
||||||
118 | |||||||
119 | You can disable link checking during development by passing a true value | ||||||
120 | to the C |
||||||
121 | |||||||
122 | =cut | ||||||
123 | |||||||
124 | 8 | 8 | 44 | use Carp; | |||
8 | 29 | ||||||
8 | 821 | ||||||
125 | 8 | 8 | 42 | use File::Spec; | |||
8 | 12 | ||||||
8 | 192 | ||||||
126 | |||||||
127 | 8 | 8 | 7090 | use Digest::HMAC; | |||
8 | 10738 | ||||||
8 | 389 | ||||||
128 | 8 | 8 | 1435 | use URI; | |||
8 | 11436 | ||||||
8 | 167 | ||||||
129 | 8 | 8 | 4438 | use URI::QueryParam; | |||
8 | 4047 | ||||||
8 | 455 | ||||||
130 | |||||||
131 | 8 | 8 | 122 | use Exporter; | |||
8 | 18 | ||||||
8 | 365 | ||||||
132 | 8 | 754 | use vars qw( | ||||
133 | @ISA | ||||||
134 | @EXPORT | ||||||
135 | $Default_Secret | ||||||
136 | 8 | 8 | 44 | ); | |||
8 | 17 | ||||||
137 | |||||||
138 | |||||||
139 | @ISA = qw(Exporter); | ||||||
140 | @EXPORT = qw(link self_link path_link link_integrity_config); | ||||||
141 | |||||||
142 | 8 | 8 | 99 | use CGI::Application; | |||
8 | 16 | ||||||
8 | 14509 | ||||||
143 | if (CGI::Application->can('new_hook')) { | ||||||
144 | CGI::Application->new_hook('invalid_checksum'); | ||||||
145 | } | ||||||
146 | |||||||
147 | =head1 METHODS | ||||||
148 | |||||||
149 | =head2 link_integrity_config | ||||||
150 | |||||||
151 | Configure the L |
||||||
152 | makes sense to configure this in the C |
||||||
153 | base class: | ||||||
154 | |||||||
155 | use CGI::Application::Plugin::LinkIntegrity; | ||||||
156 | use base 'CGI::Application'; | ||||||
157 | package My::Project; | ||||||
158 | |||||||
159 | sub setup { | ||||||
160 | my $self = shift; | ||||||
161 | |||||||
162 | $self->run_modes(['bad_user_no_biscuit']); | ||||||
163 | $self->link_integrity_config( | ||||||
164 | secret => 'some secret string known only to you and me', | ||||||
165 | link_tampered_run_mode => 'bad_user_no_biscuit', | ||||||
166 | digest_module => 'Digest::MD5', | ||||||
167 | disable => 1, | ||||||
168 | ); | ||||||
169 | } | ||||||
170 | |||||||
171 | Or you can pull in this configuration info from a config file. For | ||||||
172 | instance, with using L |
||||||
173 | |||||||
174 | use CGI::Application::Plugin::LinkIntegrity; | ||||||
175 | use CGI::Application::Plugin::Config::Context; | ||||||
176 | |||||||
177 | use base 'CGI::Application'; | ||||||
178 | package My::Project; | ||||||
179 | |||||||
180 | sub setup { | ||||||
181 | my $self = shift; | ||||||
182 | |||||||
183 | $self->conf->init( | ||||||
184 | file => 'app.conf', | ||||||
185 | driver => 'ConfigGeneral', | ||||||
186 | ); | ||||||
187 | |||||||
188 | my $config = $self->conf->context; | ||||||
189 | |||||||
190 | $self->link_integrity_config( | ||||||
191 | $config->{'LinkIntegrity'}, | ||||||
192 | additional_data => sub { | ||||||
193 | my $self = shift; | ||||||
194 | return $self->session->id; | ||||||
195 | }, | ||||||
196 | ); | ||||||
197 | |||||||
198 | my $link_tampered_rm = $config->{'LinkIntegrity'}{'link_tampered_run_mode'} || 'link_tampered'; | ||||||
199 | |||||||
200 | $self->run_modes([$link_tampered_rm]); | ||||||
201 | } | ||||||
202 | |||||||
203 | Then in your configuration file: | ||||||
204 | |||||||
205 | |
||||||
206 | secret = some REALLY secret string | ||||||
207 | link_tampered_run_mode = bad_user_no_biscuit | ||||||
208 | hash_algorithm = SHA1 | ||||||
209 | disable = 1 | ||||||
210 | |||||||
211 | |||||||
212 | This strategy allows you to enable and disable link checking on the fly | ||||||
213 | by changing the value of C |
||||||
214 | |||||||
215 | The following configuration parameters are available: | ||||||
216 | |||||||
217 | =over 4 | ||||||
218 | |||||||
219 | =item secret | ||||||
220 | |||||||
221 | A string known only to your application. At a commandline, you can | ||||||
222 | generate a secret string with md5: | ||||||
223 | |||||||
224 | $ perl -MDigest::MD5 -le"print Digest::MD5::md5_hex($$, time, rand(42));" | ||||||
225 | |||||||
226 | Or you can use Data::UUID: | ||||||
227 | |||||||
228 | $ perl -MData::UUID -le"$ug = new Data::UUID; $uuid = $ug->create; print $ug->to_string($uuid)" | ||||||
229 | |||||||
230 | If someone knows your secret string, then they can generate their own | ||||||
231 | checksums on arbitrary data that will always pass the integrity check in | ||||||
232 | your application. That's a Bad Thing, so don't let other people know | ||||||
233 | your secret string, and don't use the default secret string if you can | ||||||
234 | help it. | ||||||
235 | |||||||
236 | =item additional_data | ||||||
237 | |||||||
238 | You can pass constant additional data to the checksum generator for every link. | ||||||
239 | |||||||
240 | $self->link_integrity_config( | ||||||
241 | secret => 'really secret', | ||||||
242 | additional_data => 'some other secret data', | ||||||
243 | } | ||||||
244 | |||||||
245 | |||||||
246 | For instance, to stop one user from following a second user's link, you | ||||||
247 | can add a user-specific component to the session, such as the user's | ||||||
248 | session id: | ||||||
249 | |||||||
250 | $self->link_integrity_config( | ||||||
251 | secret => 'really secret', | ||||||
252 | additional_data => sub { | ||||||
253 | my $self = shift; | ||||||
254 | return $self->session->id; | ||||||
255 | } | ||||||
256 | } | ||||||
257 | |||||||
258 | You can pass a string instead of a subroutine. But in the case of the | ||||||
259 | user's session, a subroutine is useful so that you get the value of the | ||||||
260 | user's session at the time when the checksum is generated, not at the | ||||||
261 | time when the link integrity system is configured. | ||||||
262 | |||||||
263 | =item checksum_param | ||||||
264 | |||||||
265 | The name of the checksum parameter, which is added to the query string | ||||||
266 | and contains the cryptographic checksum of link. By default, this | ||||||
267 | parameter is named C<_checksum>. | ||||||
268 | |||||||
269 | =item link_tampered_run_mode | ||||||
270 | |||||||
271 | The run mode to go to when it has been detected that the user has | ||||||
272 | tampered with the link. The default is C |
||||||
273 | |||||||
274 | See L<"The link_tampered Run Mode">, below. | ||||||
275 | |||||||
276 | =item digest_module | ||||||
277 | |||||||
278 | Which digest module to use to create the checksum. Typically, this will | ||||||
279 | be either C |
||||||
280 | supported by C |
||||||
281 | |||||||
282 | The default C |
||||||
283 | |||||||
284 | =item checksum_generator | ||||||
285 | |||||||
286 | If you want to provide a custom subroutine to make your own checksums, | ||||||
287 | you can define your own subroutine do it via the C |
||||||
288 | Here's an example of one that uses Digest::SHA2: | ||||||
289 | |||||||
290 | $self->link_integrity_config( | ||||||
291 | checksum_generator => sub { | ||||||
292 | my ($url, $secret) = @_; | ||||||
293 | require Digest::SHA2; | ||||||
294 | |||||||
295 | my $ctx = Digest::SHA2->new(); | ||||||
296 | $ctx->add($url . $secret); | ||||||
297 | |||||||
298 | return $ctx->hexdigest; | ||||||
299 | }, | ||||||
300 | ); | ||||||
301 | |||||||
302 | =item disable | ||||||
303 | |||||||
304 | You can disable link checking entirely by setting C |
||||||
305 | value. This can be useful when you are developing or debugging the | ||||||
306 | application and you want the ability to tweak URL params without getting | ||||||
307 | busted. | ||||||
308 | |||||||
309 | =back | ||||||
310 | |||||||
311 | =cut | ||||||
312 | |||||||
313 | my %Config_Defaults = ( | ||||||
314 | secret => undef, | ||||||
315 | checksum_param => '_checksum', | ||||||
316 | link_tampered_run_mode => undef, | ||||||
317 | digest_module => 'Digest::MD5', | ||||||
318 | disable => undef, | ||||||
319 | checksum_generator => undef, | ||||||
320 | additional_data => undef, | ||||||
321 | ); | ||||||
322 | |||||||
323 | sub link_integrity_config { | ||||||
324 | 66 | 66 | 1 | 132355 | my $self = shift; | ||
325 | |||||||
326 | 66 | 151 | my $caller = scalar(caller); | ||||
327 | |||||||
328 | 66 | 252 | $self->new_hook('invalid_checksum'); | ||||
329 | 66 | 598 | $caller->add_callback('prerun', \&_check_link_integrity); | ||||
330 | |||||||
331 | 66 | 764 | my $args; | ||||
332 | 66 | 50 | 189 | if (ref $_[0] eq 'HASH') { | |||
333 | 0 | 0 | $args = $_[0]; | ||||
334 | } | ||||||
335 | else { | ||||||
336 | 66 | 215 | $args = { @_ }; | ||||
337 | } | ||||||
338 | |||||||
339 | # Clear config | ||||||
340 | 66 | 202 | undef $self->{__PACKAGE__}{__CONFIG}; | ||||
341 | 66 | 176 | my $config = _get_config($self, $args); | ||||
342 | |||||||
343 | 66 | 50 | 170 | $config->{'link_tampered_run_mode'} ||= 'link_tampered'; | |||
344 | |||||||
345 | 66 | 369 | my %run_modes = $self->run_modes; | ||||
346 | 66 | 100 | 901 | unless ($run_modes{$config->{'link_tampered_run_mode'}}) { | |||
347 | $self->run_modes($config->{'link_tampered_run_mode'} => sub { | ||||||
348 | 3 | 3 | 232 | return 'Access Denied'; |
|||
349 | 12 | 81 | }); | ||||
350 | } | ||||||
351 | 66 | 365 | %run_modes = $self->run_modes; | ||||
352 | |||||||
353 | } | ||||||
354 | |||||||
355 | sub _get_config { | ||||||
356 | 206 | 206 | 310 | my ($self, $args) = @_; | |||
357 | |||||||
358 | 206 | 100 | 640 | if ($self->{__PACKAGE__}{__CONFIG}) { | |||
359 | 140 | 403 | return $self->{__PACKAGE__}{__CONFIG}; | ||||
360 | } | ||||||
361 | 66 | 445 | my $config = $self->{__PACKAGE__}{__CONFIG} = { %Config_Defaults }; | ||||
362 | |||||||
363 | 66 | 50 | 228 | if ($args) { | |||
364 | # Check that all key names are valid | ||||||
365 | 66 | 199 | foreach my $key (keys %$args) { | ||||
366 | 93 | 50 | 237 | unless (exists $config->{$key}) { | |||
367 | 0 | 0 | croak "CAP::LinkIntegrity: Bad configuration key: $key\n"; | ||||
368 | } | ||||||
369 | 93 | 237 | $config->{$key} = $args->{$key}; | ||||
370 | } | ||||||
371 | # Check that checksum_generator is coderef | ||||||
372 | 66 | 100 | 217 | if (exists $args->{'checksum_generator'}) { | |||
373 | 1 | 50 | 4 | unless (ref $args->{'checksum_generator'} eq 'CODE') { | |||
374 | 0 | 0 | croak "CAP::LinkIntegrity: checksum_generator must be coderef\n"; | ||||
375 | } | ||||||
376 | } | ||||||
377 | } | ||||||
378 | 66 | 100 | 306 | $config->{'link_tampered_run_mode'} ||= 'link_tampered'; | |||
379 | |||||||
380 | 66 | 50 | 225 | $config->{'secret'} || croak "CAP::LinkIntegrity - You need to provide a secret string to link_integrity_config"; | |||
381 | |||||||
382 | 66 | 154 | return $config; | ||||
383 | } | ||||||
384 | |||||||
385 | =head2 link | ||||||
386 | |||||||
387 | Create a link, and add a checksum to it. | ||||||
388 | |||||||
389 | You can add parameters to the link directly in the URL: | ||||||
390 | |||||||
391 | my $link = $self->link('/cgi-bin/app.cgi?var=value&var2=value2'); | ||||||
392 | |||||||
393 | Or you can add them as a hash of parameters after the URL: | ||||||
394 | |||||||
395 | my $link = $self->link( | ||||||
396 | '/cgi-bin/app.cgi', | ||||||
397 | 'param1' => 'value', | ||||||
398 | 'param2' => 'value2', | ||||||
399 | ); | ||||||
400 | |||||||
401 | =cut | ||||||
402 | |||||||
403 | sub link { | ||||||
404 | 18 | 18 | 1 | 3763 | my $self = shift; | ||
405 | 18 | 31 | my $uri = shift; | ||||
406 | |||||||
407 | 18 | 46 | my $config = _get_config($self); | ||||
408 | |||||||
409 | 18 | 86 | $uri = URI->new($uri, 'http'); | ||||
410 | |||||||
411 | 18 | 26813 | my @query_form = $uri->query_form; | ||||
412 | |||||||
413 | 18 | 1446 | push @query_form, @_; | ||||
414 | |||||||
415 | 18 | 42 | my $additional_data = $config->{'additional_data'}; | ||||
416 | 18 | 100 | 69 | $additional_data = $additional_data->($self) if ref $additional_data eq 'CODE'; | |||
417 | |||||||
418 | 18 | 73 | my $checksum = _hmac($self, $uri, $additional_data); | ||||
419 | |||||||
420 | 17 | 66 | $uri->query_form(@query_form); | ||||
421 | 17 | 1198 | $uri->query_param_append($config->{'checksum_param'} => $checksum); | ||||
422 | |||||||
423 | 17 | 2708 | return $uri; | ||||
424 | } | ||||||
425 | |||||||
426 | sub _hmac { | ||||||
427 | 65 | 65 | 100 | my $self = shift; | |||
428 | 65 | 95 | my $uri = shift; | ||||
429 | 65 | 92 | my $additional_data = shift; | ||||
430 | |||||||
431 | 65 | 142 | my $config = _get_config($self); | ||||
432 | |||||||
433 | 65 | 144 | my $secret = $config->{'secret'}; | ||||
434 | |||||||
435 | 65 | 86 | my $digest; | ||||
436 | 65 | 100 | 179 | if ($config->{'checksum_generator'}) { | |||
437 | 1 | 4 | $digest = $config->{'checksum_generator'}->($secret, $uri, $additional_data); | ||||
438 | } | ||||||
439 | else { | ||||||
440 | 64 | 66 | 817 | my $digest_module = $config->{'digest_module'} || croak "CAP::LinkIntegrity: digest_module not configured"; | |||
441 | 63 | 6669 | eval "require $digest_module"; | ||||
442 | 63 | 50 | 270 | if ($@) { | |||
443 | 0 | 0 | carp "CAP::LinkIntegrity: Requested digest_module ($digest_module) not installed"; | ||||
444 | } | ||||||
445 | |||||||
446 | 63 | 327 | my $hmac = Digest::HMAC->new($secret, $digest_module); | ||||
447 | |||||||
448 | # Add all elements of the URL | ||||||
449 | 63 | 100 | 1936 | $hmac->add($uri->scheme || ''); | |||
450 | 63 | 100 | 2009 | $hmac->add($uri->authority || ''); | |||
451 | 63 | 50 | 1405 | $hmac->add($uri->port || ''); | |||
452 | 63 | 50 | 2211 | $hmac->add($uri->path || ''); | |||
453 | |||||||
454 | 63 | 1412 | foreach my $key (sort $uri->query_param) { | ||||
455 | 121 | 11464 | $hmac->add('key'); | ||||
456 | 121 | 769 | $hmac->add($key); | ||||
457 | 121 | 711 | $hmac->add('values'); | ||||
458 | 121 | 746 | $hmac->add($_) for sort $uri->query_param($key); | ||||
459 | } | ||||||
460 | |||||||
461 | 63 | 100 | 5859 | $hmac->add($additional_data || ''); | |||
462 | 63 | 432 | $digest = $hmac->hexdigest; | ||||
463 | } | ||||||
464 | 64 | 1384 | return $digest; | ||||
465 | } | ||||||
466 | |||||||
467 | =head2 self_link | ||||||
468 | |||||||
469 | Make a link to the current application, with optional parameters, and | ||||||
470 | add a checksum to the URL. | ||||||
471 | |||||||
472 | my $link = $self->self_link( | ||||||
473 | 'param1' => 'value1', | ||||||
474 | 'param2' => 'value2', | ||||||
475 | ); | ||||||
476 | |||||||
477 | C |
||||||
478 | For instance if the current URL is: | ||||||
479 | |||||||
480 | /cgi-bin/app.cgi/some/path?foo=bar # PATH_INFO is 'some/path' | ||||||
481 | |||||||
482 | Calling: | ||||||
483 | |||||||
484 | $self->self_link('bar' => 'baz'); | ||||||
485 | |||||||
486 | Will produce the URL: | ||||||
487 | |||||||
488 | /cgi-bin/app.cgi/some/path?bar=baz | ||||||
489 | |||||||
490 | If you want to remove the C |
||||||
491 | value, use L |
||||||
492 | |||||||
493 | =cut | ||||||
494 | |||||||
495 | sub self_link { | ||||||
496 | 2 | 2 | 1 | 1470 | my $self = shift; | ||
497 | |||||||
498 | 2 | 8 | my $uri = URI->new($self->query->url(-path_info => 1)); | ||||
499 | |||||||
500 | 2 | 100 | 2393 | $uri->query_form(@_) if @_; | |||
501 | |||||||
502 | 2 | 79 | return $self->link($uri); | ||||
503 | } | ||||||
504 | |||||||
505 | =head2 path_link | ||||||
506 | |||||||
507 | Calling C |
||||||
508 | the current value of C |
||||||
509 | |||||||
510 | my $link = $self->path_link( | ||||||
511 | '/new/path', | ||||||
512 | 'param1' => 'value1', | ||||||
513 | 'param2' => 'value2', | ||||||
514 | ); | ||||||
515 | |||||||
516 | For instance if the current URL is: | ||||||
517 | |||||||
518 | /cgi-bin/app.cgi/some/path?foo=bar # PATH_INFO is 'some/path' | ||||||
519 | |||||||
520 | Calling: | ||||||
521 | |||||||
522 | $self->path_link('/new/path'); | ||||||
523 | |||||||
524 | Will produce the URL: | ||||||
525 | |||||||
526 | /cgi-bin/app.cgi/new/path?foo=bar | ||||||
527 | |||||||
528 | If you want to remove C |
||||||
529 | |||||||
530 | $self->path_link; | ||||||
531 | $self->path_link(undef, 'param1' => 'val1', 'param2 => 'val2' ...); | ||||||
532 | $self->path_link('', 'param1' => 'val1', 'param2 => 'val2' ...); | ||||||
533 | |||||||
534 | If you want to keep the existing C |
||||||
535 | current application, use L |
||||||
536 | |||||||
537 | =cut | ||||||
538 | |||||||
539 | sub path_link { | ||||||
540 | 4 | 4 | 1 | 8426 | my $self = shift; | ||
541 | 4 | 8 | my $path_info = shift; | ||||
542 | |||||||
543 | 4 | 6 | my $uri; | ||||
544 | |||||||
545 | 4 | 20 | $uri = URI->new($self->query->url); | ||||
546 | 4 | 100 | 6430 | if ($path_info) { | |||
547 | |||||||
548 | # Convert into an array of path elements | ||||||
549 | 1 | 23 | my @path_info = File::Spec->splitdir($path_info); | ||||
550 | |||||||
551 | # Remove the first element if it is the empty root element | ||||||
552 | 1 | 50 | 5 | shift @path_info unless $path_info[0]; | |||
553 | |||||||
554 | 1 | 13 | $uri->path_segments($uri->path_segments, @path_info); | ||||
555 | } | ||||||
556 | |||||||
557 | 4 | 100 | 187 | $uri->query_form(@_) if @_; | |||
558 | |||||||
559 | 4 | 362 | return $self->link($uri); | ||||
560 | } | ||||||
561 | |||||||
562 | sub _check_link_integrity { | ||||||
563 | 57 | 57 | 119041 | my $self = shift; | |||
564 | |||||||
565 | 57 | 50 | 212 | unless ($self->{__PACKAGE__}{__CONFIG}) { | |||
566 | 0 | 0 | croak "CAP::LinkIntegrity - You need to call link_integrity_config before 'prerun' (e.g. in start or cgiapp_init)\n"; | ||||
567 | } | ||||||
568 | |||||||
569 | 57 | 136 | my $config = _get_config($self); | ||||
570 | |||||||
571 | |||||||
572 | 57 | 100 | 159 | return if $config->{'disable'}; | |||
573 | |||||||
574 | 52 | 167 | my $uri = URI->new($self->query->url(-path_info => 1)); | ||||
575 | |||||||
576 | 52 | 128668 | my @params; | ||||
577 | |||||||
578 | # Entry point #1: if the URL contains no params we let it through | ||||||
579 | 52 | 100 | 203 | return unless $self->query->url_param; | |||
580 | |||||||
581 | # Entry point #2: if the URL contains only a single param named 'keywords' | ||||||
582 | # and this param has no value. This is due to the fact that CGI.pm adds | ||||||
583 | # a blank 'keywords' param when the QUERY_STRING is blank | ||||||
584 | |||||||
585 | 48 | 16664 | my @param = $self->query->url_param; | ||||
586 | 48 | 100 | 66 | 3374 | if (@param == 1 and $param[0] eq 'keywords') { | ||
587 | 4 | 13 | my $keywords = $self->query->param('keywords'); | ||||
588 | 4 | 100 | 66 | 119 | return if !defined $keywords or $keywords eq ''; | ||
589 | } | ||||||
590 | |||||||
591 | 47 | 161 | foreach my $name (sort $self->query->url_param) { | ||||
592 | 129 | 3365 | foreach my $val (sort $self->query->url_param($name)) { | ||||
593 | 176 | 10519 | push @params, $name, $val; | ||||
594 | } | ||||||
595 | } | ||||||
596 | |||||||
597 | 47 | 238 | $uri->query_form(@params); | ||||
598 | |||||||
599 | 47 | 4558 | my $uri_checksum = $uri->query_param_delete($config->{'checksum_param'}); | ||||
600 | 47 | 7483 | my $expected_checksum = _hmac($self, $uri, $config->{'additional_data'}); | ||||
601 | |||||||
602 | 47 | 100 | 100 | 350 | if (($uri_checksum || '') ne ($expected_checksum || '')) { | ||
50 | |||||||
603 | 25 | 113 | $self->prerun_mode($config->{'link_tampered_run_mode'}); | ||||
604 | 25 | 345 | $self->call_hook('invalid_checksum'); | ||||
605 | } | ||||||
606 | } | ||||||
607 | |||||||
608 | |||||||
609 | =head1 RUN MODES | ||||||
610 | |||||||
611 | =head2 The link_tampered Run Mode | ||||||
612 | |||||||
613 | Your application is redirected to this run mode when it has been | ||||||
614 | detected that the user has tampered with the link. You can change the | ||||||
615 | name of this run mode by changing the value of the | ||||||
616 | C |
||||||
617 | |||||||
618 | L |
||||||
619 | C |
||||||
620 | warning text. | ||||||
621 | |||||||
622 | You can define your own as follows: | ||||||
623 | |||||||
624 | sub link_tampered { | ||||||
625 | my $self = shift; | ||||||
626 | my $template = $self->load_template('stern_talking_to'); | ||||||
627 | return $template->output; | ||||||
628 | } | ||||||
629 | |||||||
630 | =head1 HOOKS | ||||||
631 | |||||||
632 | When a link is followed that doesn't match the checksum, the | ||||||
633 | C |
||||||
634 | to do some cleanup such as deleting the user's session. For instance: | ||||||
635 | |||||||
636 | sub setup { | ||||||
637 | my $self = shift; | ||||||
638 | $self->add_callback('invalid_checksum' => \&bad_user); | ||||||
639 | } | ||||||
640 | |||||||
641 | sub bad_user { | ||||||
642 | my $self = shift; | ||||||
643 | |||||||
644 | # The user has been messing with the URLs, possibly trying to | ||||||
645 | # break into the system. We don't tolerate this behaviour. | ||||||
646 | # So we delete the user's session: | ||||||
647 | |||||||
648 | $self->session->delete; | ||||||
649 | } | ||||||
650 | |||||||
651 | =head1 AUTHOR | ||||||
652 | |||||||
653 | Michael Graham, C<< |
||||||
654 | |||||||
655 | =head1 ACKNOWLEDGEMENTS | ||||||
656 | |||||||
657 | This module was based on the checksum feature originally built into | ||||||
658 | Richard Dice's L |
||||||
659 | |||||||
660 | =head1 BUGS | ||||||
661 | |||||||
662 | Please report any bugs or feature requests to | ||||||
663 | C |
||||||
664 | L |
||||||
665 | be notified of progress on your bug as I make changes. | ||||||
666 | |||||||
667 | =head1 COPYRIGHT & LICENSE | ||||||
668 | |||||||
669 | Copyright 2005 Michael Graham, All Rights Reserved. | ||||||
670 | |||||||
671 | This program is free software; you can redistribute it and/or modify it | ||||||
672 | under the same terms as Perl itself. | ||||||
673 | |||||||
674 | =cut | ||||||
675 | |||||||
676 | 1; # End of CGI::Application::Plugin::LinkIntegrity |