Changeset 217
- Timestamp:
- 09/29/06 21:14:53 (2 years ago)
- Files:
-
- trunk/lib/AxKit2/Plugin.pm (modified) (4 diffs)
- trunk/plugins/authenticate (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/lib/AxKit2/Plugin.pm
r216 r217 121 121 } 122 122 123 124 my %_HANDLER_ATTRIB; 125 126 sub Stacked : ATTR(CODE) { 127 my ($package, $symbol, $referent) = @_; 128 $_HANDLER_ATTRIB{$referent} = 1; 129 } 130 123 131 sub dispatch_message { 124 132 my $self = shift; 125 133 my $message = shift; 126 134 if (my $sub = $self->can("message_$message")) { 135 $sub->($self,@_), return DECLINED if ($_HANDLER_ATTRIB{$sub}); 127 136 return OK, $sub->($self,@_); 128 137 } … … 295 304 Retrieve the name of the currently executing hook 296 305 306 =head2 C<< $plugin->notes >> 307 308 If you need to store plugin-private data associated with a request, you can store 309 them with C<< $plugin->notes($key,$value) >> and retrieve them via C<< $plugin->notes($key) >>. 310 It works exactly like C<< $client->notes(...) >>, except that the key is made unique for 311 each plugin. 312 313 =head2 C<< $plugin->send >> 314 315 This is the interface to user-defined callbacks and message passing. Plugins can define 316 messages they handle like this: 317 318 sub message_login { # sent by plugins/authenticate 319 my ($self, $user) = @_; 320 # let an imaginary database setting do something on login 321 $self->config->database->login($user); 322 } 323 324 Other plugins can then send a message using C<< $plugin->send('login',$user) >>. This is 325 intended for typical callback situations, but it also allows plugins to offer services 326 to other plugins, like this: 327 328 my $cached_object = $plugin->send('cache',$key); 329 330 Since all plugins are searched for a corresponding message handler, plugins are independent 331 of the actual implementation of such services. 332 333 By default, the first message handler that is found is run and it's return value returned. 334 If you want your handler to stack, i.e., that other handlers are run after it, specify the 335 attribute C<Stacked> on the sub: 336 337 sub message_login : Stacked { 338 my ($self, $user) = @_; 339 # log it somewhere special, but do not interfere with regular login processing. 340 open(my $fh, '>>', 'userlog'); print $fh time()." ".$user."\n"; close($fh); 341 } 342 343 Of course, the return value of stacked handlers is ignored. 344 345 If you need even more complex processing, see C<< hook_handler >> below. 346 297 347 =head1 CONFIGURATION DIRECTIVES 298 348 … … 351 401 For full details and more ways of designing configuration directives, see L<AxKit2::Config>. 352 402 403 =head1 OTHER FACILITIES 404 405 Since AxKit2 is based on L<Danga::Socket>, those facilities are also available to plugins. 406 You should read it's documentation for details, but two use cases are rather common: 407 408 =head2 Timers 409 410 Have a custom callback at (roughly) a certain time from now: 411 412 # add a timed callback 413 my $timer = Danga::Socket->AddTimer($seconds, $subref); 414 # later, if you decide the timer is no longer needed 415 $timer->cancel; 416 417 Pass a closure if you need values from C<$plugin>, as the callback is called 418 without arguments or C<$self> reference. 419 420 =head2 Watching additional FDs 421 422 If you want to watch other file descriptors for IO, for example a permanent 423 network connection to some other host, or a subprocess that you spawned for 424 some asynchronous processing, add a new package at the end of your plugin, 425 like this: 426 427 # ... your regular plugin code ... 428 429 package My::Plugin::SubprocessEvent 430 use base 'Danga::Socket'; 431 # ... and so on, copy the example code from Danga::Socket docs 432 433 Since watching a FD happens in parallel to other requests, you will have to 434 use global variables or similar to communicate with your plugin. 435 353 436 =head1 AVAILABLE HOOKS 354 437 … … 612 695 613 696 =back 697 698 699 =head2 message 700 701 Params: MESSAGE_NAME, ARGUMENTS 702 703 Called whenever a plugin C<send>'s a message. 704 705 Return Value: 706 707 =over 4 708 709 =item * C<DECLINED> - go on dispatching the message 710 711 =item * C<OK>, I<RETVAL> - Finish this message dispatch, return I<RETVAL> to caller. 712 713 =back 714 614 715 615 716 trunk/plugins/authenticate
r216 r217 138 138 139 139 Digest is recommended, since it is the best you can get short of going 140 the SSL route, i.e. it protects you from everything but man-in-the-middle attacks and 141 eavesdropping on HTML data, which only SSL can defend against, at a price. 140 the SSL route, i.e. it protects you from everything but eavesdropping 141 on transferred data, which only SSL can defend against, at a price. Not only 142 does it protect your password (an attacker can never read or guess your password 143 just by sniffing on the line), it also effectively prevents more advanced 144 attacks like man-in-the-middle or replay attacks. 142 145 143 146 Basic provides practically no security, since anyone snooping on your connection can 144 147 read the password in (superficially obfuscated) plain text. It is widely 145 148 supported. This is relevant if you expect media players and other embedded HTTP 146 clients to read your data. All regular web browsers know how to speak Digest. 149 clients to read your data that may not yet know Digest. All regular web browsers 150 know how to speak Digest, so this is probably not an issue anymore. 151 152 Digest authentication needs a little bit of server-side state to be really secure. 153 Each session uses about 70 bytes. This may sound a bit non-HTTPish, but in return 154 the possibilities of Digest authentication are used for maximum security. Sessions 155 that are not explicitly logged out are purged after one to two days. 147 156 148 157 =head2 C<AuthRealm> I<realm> … … 217 226 } 218 227 219 my $_SERVER_SECRET = rand().rand(); # FIXME: get some kind of cryptographically secure randomness 220 my %_NC; 228 sub _secret() { 229 # MersenneTwister is not a cryptographically secure PRNG unless fed through a hash function. 230 return md5(&rand()); 231 } 232 233 # a secret used to protect first-time requests 234 my $_SERVER_SECRET; 235 236 sub init { 237 my $self = shift; 238 239 # Try to get secure random numbers. Without, security does not withstand reasonably determined attack. 240 eval { 241 require Math::Random::MT::Auto; 242 Math::Random::MT::Auto->import('rand','srand'); 243 my $init; 244 eval { &srand('/dev/random'); $init = 1; } if -c "/dev/random"; 245 eval { require Win32::API; &srand('win32'); $init = 1; }; 246 $self->log(LOGWARN,"Could not seed PRNG. You need to call Math::Random::MT::Auto::srand yourself.") unless $init; 247 }; 248 $self->log(LOGWARN,"Could not load suitable PRNG. Digest auth will be insecure. Please install Math::Random::MT::Auto.") if ($@); 249 250 $_SERVER_SECRET = _secret; 251 cleanup(); 252 } 253 254 # records nonce-count, timestamp, secret and username for sessions 255 my %_SESSIONS; 256 257 sub cleanup { 258 my $limit = time()-86400; 259 260 foreach my $session (keys %_SESSIONS) { 261 my ($nc, $timestamp) = @{$_SESSIONS{$session}}; 262 # remove sessions that haven't been touched for at least a day. 263 delete $_SESSIONS{$session} if ($timestamp <= $limit); 264 } 265 266 Danga::Socket->AddTimer(86400,\&cleanup); 267 } 221 268 222 269 =head2 C<< logout >> … … 236 283 might want to lock out users after a certain number of failed attempts. 237 284 238 =head2 C<< login_timeout >>239 240 This message is sent whenever a login session times out. This can happen I<after> the user241 has logged in with another session, but it will sooner or later be sent for every session242 not logged out via message C<logout>.243 244 285 =cut 245 286 246 287 sub message_logout { 247 my ($self ) = @_;248 my $session = $self->client->headers_in->header('Digest-Session');288 my ($self, $session) = @_; 289 $session = $self->client->headers_in->header('Digest-Session') unless defined $session; 249 290 return unless defined $session; 250 for my $nonce (grep { m/^\d+:\Q$session:/ } keys %_NC) { 251 delete $_NC{$nonce}; 291 delete $_SESSIONS{$session}; 292 } 293 294 sub send_digest_challenge { 295 my ($self, $session, $user) = @_; 296 my $secret = $_SERVER_SECRET; 297 298 my $timestamp = time(); 299 if (exists $_SESSIONS{$session}) { 300 $_SESSIONS{$session}[0] = 1; 301 $_SESSIONS{$session}[1] = int($timestamp); 302 $secret = $_SESSIONS{$session}[2] = _secret; 303 } else { 304 undef $session; 252 305 } 253 } 254 255 sub send_digest_challenge { 256 my ($self, $oldnonce, $session) = @_; 257 delete $_NC{$oldnonce} if defined $oldnonce; # This is not failsafe in the face of pipelined requests; it doesn't need to be. 258 my $nonce = time().':'.(defined $session?$session:md5_hex(rand(time()))); 259 $nonce .= ":".md5($nonce.":".$_SERVER_SECRET); 260 $_NC{$nonce} = 0 if defined $session; 306 307 my $nonce = $timestamp . ":" . ( defined $session? $session : _secret ) . ":" . $user . ":"; 308 $nonce .= md5($nonce . $secret); 261 309 my $header = "Digest realm=" . quoted_string($self->config->auth_realm) . ", " . 262 310 "domain=".quoted_string($self->config->auth_domain).", " . … … 336 384 } 337 385 386 # Cheap tests first, save processing time on mismatch. 387 338 388 # This could be a hacking attempt, a config change, or an ambiguous config. 339 389 return $self->send_digest_challenge if ($param{realm} ne $self->config->auth_realm); 340 390 391 # Unknown username/realm combination 341 392 my $hash = $self->config->auth_file->{$param{username}.':'.$param{realm}}; 342 # Unknown username/realm combination343 393 return $self->send_digest_challenge if (!$hash); 344 394 395 # Validate nonce. 345 396 $param{nonce} = decode_base64($param{nonce}); 346 my ($timestamp,$session,$nonce) = split(/:/,$param{nonce},3); 397 my ($timestamp,$session,$user,$nonce) = split(/:/,$param{nonce},4); 398 347 399 # This can't possibly be one of our nonces, so there is probably something fishy going on. 348 400 return $self->send_digest_forbidden("impossible nonce") if (int($timestamp) ne $timestamp || !defined $nonce || int($timestamp) > time()); 349 401 402 my $s = $_SESSIONS{$session}; 403 if ($s) { 404 # Validate nc if present. 405 return $self->send_digest_forbidden("replay attack") if $param{qop} && $param{nc} ne sprintf("%08x",$$s[0]); 406 # Timestamp invalid: replay attack. 407 return $self->send_digest_forbidden("invalid timestamp") if $timestamp != $$s[1]; 408 # Username mismatch: attempt to take over different user's session 409 return $self->send_digest_forbidden("user mismatch in server nonce") if $param{username} ne $user; 410 } else { 411 # Replay attack, expired or logged out session. Force relogin. 412 return $self->send_digest_challenge if $param{qop} && $param{nc} ne '00000001'; 413 # Username mismatch: attempt to take over different user's session, or ancient session with old client. 414 return $self->send_digest_challenge if length($user); 415 } 416 417 # More expensive checks. 418 419 # If the nonce doesn't match what it should be, something fishy is going on: A user tries to act as a different user, 420 # take over another user's session id, or it could be a replay attack. 421 # Unfortunately, this can also happen after a server restart. Force a relogin, however. 422 my $secret = ( $s? $$s[2] : $_SERVER_SECRET ); 423 $nonce = "$timestamp:$session:$user:" . md5("$timestamp:$session:$user:$secret"); 424 return $self->send_digest_challenge if ($param{nonce} ne $nonce); 425 426 # The heart of Digest auth: check the response hash. 350 427 my $bodyhash = ($self->notes('bodyhash')? $self->notes('bodyhash')->hexdigest : '0' x 32); 351 428 my $A2 = md5_hex( $hd->request_method . ':' . $param{uri} . ($param{qop} eq 'auth-int'? ':' . $bodyhash : '') ); … … 353 430 ($param{qop}? $param{nc} . ':' . $param{cnonce} . ':' . $param{qop} . ':' : '') . 354 431 $A2 ); 355 # Wrong password, or worse. 356 $self->send('login_failed',$param{username}), return $self->send_digest_challenge if ($response ne $param{response}); 357 432 433 if ($response ne $param{response}) { 434 # Wrong password. 435 $self->send('login_failed',$param{username}), return $self->send_digest_challenge if (!$s); 436 # Hacking attempt. 437 $self->send_digest_forbidden("response mismatch"); 438 } 439 440 # At this point, we know for sure that a known user is (or was) involved with this request. 441 # Only from here on we do checks that establish/alter server-side state. 442 443 # All clients get a new nonce once per hour. 444 return $self->send_digest_challenge($session,$param{username}) if $timestamp < time()-3600; 445 446 # Create response authentication. 358 447 $A2 = md5_hex( ':' . $param{uri} ); # TODO: provide auth-int? . ($param{qop} eq 'auth-int'? ':' . $bodyhash : '') 359 448 $response = md5_hex( $hash->[0] . ':' . encode_base64($param{nonce},"") . ':' . … … 362 451 $self->client->headers_out->header('Authentication-Info',"qop=auth, cnonce=".quoted_string($param{cnonce}).", nc=$param{nc}, rspauth=".quoted_string($response)); 363 452 364 $nonce = $timestamp.':'.$session.':'.md5($timestamp.':'.$session.':'.$_SERVER_SECRET);365 # This could be a hacking attempt, or a server restart.366 return $self->send_digest_challenge($param{nonce},$session) if ($param{nonce} ne $nonce);367 368 453 $hd->header('Digest-Session',$session); 369 if (!defined $_NC{$nonce}) { 370 # Force relogin if the first challenge response takes too long (2 minutes). 371 return $self->send_digest_challenge if int($timestamp) < time()-120; 372 $_NC{$nonce} = 0; 454 if (!defined $_SESSIONS{$session}[0]) { 455 # successful login, establish server-side state 456 $_SESSIONS{$session} = [ -1 ]; 373 457 $self->send('login',$param{username}); 458 # need another round-trip, since we want to tie the session id to the user name 459 # but avoid as much server-side state as possible without compromising security. 460 return $self->send_digest_challenge($session,$param{username}); 374 461 } 375 462 … … 377 464 if (!$param{qop} || $param{qop} eq 'auth,auth-int') { 378 465 # Change nonce once per minute to reduce replay attack time window. 379 return $self->send_digest_challenge($ param{nonce},$session) if $timestamp < time()-60;466 return $self->send_digest_challenge($session,$param{username}) if $timestamp < time()-60; 380 467 return OK; 381 468 } 382 469 383 # Replay attack, or logged out session. 384 return $self->send_digest_challenge if (!$_NC{$nonce} && $param{nc} ne '00000001'); 385 # Replay attack. 386 return $self->send_digest_forbidden("replay attack") if ($param{nc} ne sprintf("%08x",++$_NC{$nonce})); 470 # Increase nonce-count if authentication passed. 471 $$s[0]++; 387 472 388 473 # All checks passed.
