+
+sub check_digest_auth {
+ my ($r, $auth) = @_;
+
+ # We're a bit more liberal than RFC2069 in the parsing here, allowing
+ # quoted strings everywhere.
+ my %auth = ();
+ while ($auth =~ s/^ ([a-zA-Z]+) # key
+ =
+ (
+ [^",]* # either something that doesn't contain comma or quotes
+ |
+ " ( [^"\\] | \\ . ) * " # or a full quoted string
+ )
+ (?: (?: , \s* ) + | $ ) # delimiter(s), or end of string
+ //x) {
+ my ($key, $value) = ($1, $2);
+ if ($value =~ /^"(.*)"$/) {
+ $value = $1;
+ $value =~ s/\\(.)/$1/g;
+ }
+ $auth{$key} = $value;
+ }
+ unless (exists($auth{'username'}) &&
+ exists($auth{'uri'}) &&
+ exists($auth{'nonce'}) &&
+ exists($auth{'opaque'}) &&
+ exists($auth{'response'})) {
+ output_401($r);
+ return undef;
+ }
+ if ($r->uri ne $auth{'uri'}) {
+ output_401($r);
+ return undef;
+ }
+
+ # Verify that the opaque data does indeed look like a timestamp, and that the nonce
+ # is indeed a signed version of it.
+ if ($auth{'opaque'} !~ /^\d+$/) {
+ output_401($r);
+ return undef;
+ }
+ my $compare_nonce = Digest::HMAC_SHA1->hmac_sha1_hex($auth{'opaque'}, $Sesse::pr0n::Config::db_password);
+ if ($auth{'nonce'} ne $compare_nonce) {
+ output_401($r);
+ return undef;
+ }
+
+ # Now look up the user's HA1 from the database, and calculate HA2.
+ my ($user, $takenby) = extract_takenby($auth{'username'});
+ my $ref = $dbh->selectrow_hashref('SELECT digest_ha1_hex FROM users WHERE username=? AND vhost=?',
+ undef, $user, $r->get_server_name);
+ if (!defined($ref)) {
+ output_401($r);
+ return undef;
+ }
+ if (!defined($ref->{'digest_ha1_hex'}) || $ref->{'digest_ha1_hex'} !~ /^[0-9a-f]{32}$/) {
+ # A user that exists but has empty HA1 is a user that's not
+ # ready for digest auth, so we hack it and resend 401,
+ # only this time without digest auth.
+ output_401($r, DigestAuth => 0);
+ return undef;
+ }
+ my $ha1 = $ref->{'digest_ha1_hex'};
+ my $ha2 = Digest::MD5::md5_hex($r->method . ':' . $auth{'uri'});
+ my $response;
+ if (exists($auth{'qop'}) && $auth{'qop'} eq 'auth') {
+ unless (exists($auth{'nc'}) && exists($auth{'cnonce'})) {
+ output_401($r);
+ return undef;
+ }
+
+ $response = $ha1;
+ $response .= ':' . $auth{'nonce'};
+ $response .= ':' . $auth{'nc'};
+ $response .= ':' . $auth{'cnonce'};
+ $response .= ':' . $auth{'qop'};
+ $response .= ':' . $ha2;
+ } else {
+ $response = $ha1;
+ $response .= ':' . $auth{'nonce'};
+ $response .= ':' . $ha2;
+ }
+ if ($auth{'response'} ne Digest::MD5::md5_hex($response)) {
+ output_401($r);
+ return undef;
+ }
+
+ # OK, everything is good, and there's only one thing we need to check: That the nonce
+ # isn't too old. If it is, but everything else is ok, we tell the browser that and it
+ # will re-encrypt with the new nonce.
+ my $timediff = time - $auth{'opaque'};
+ if ($timediff < 0 || $timediff > 300) {
+ output_401($r, StaleNonce => 1);
+ return undef;
+ }
+
+ return ($user, $takenby);
+}
+
+sub extract_takenby {
+ my ($user) = shift;
+
+ # WinXP is stupid :-)
+ if ($user =~ /^.*\\(.*)$/) {
+ $user = $1;
+ }
+
+ my $takenby;
+ if ($user =~ /^([a-zA-Z0-9^_-]+)\@([a-zA-Z0-9^_-]+)$/) {
+ $user = $1;
+ $takenby = $2;
+ } else {
+ ($takenby = $user) =~ s/^([a-zA-Z])/uc($1)/e;
+ }
+
+ return ($user, $takenby);
+}