| 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 |