File Coverage

blib/lib/Mojo/UserAgent/DigestAuth.pm
Criterion Covered Total %
statement 47 53 88.6
branch 5 10 50.0
condition 8 18 44.4
subroutine 9 9 100.0
pod n/a
total 69 90 76.6


line stmt bran cond sub pod time code
1             package Mojo::UserAgent::DigestAuth;
2              
3             =head1 NAME
4              
5             Mojo::UserAgent::DigestAuth - Allow Mojo::UserAgent to execute digest auth requests
6              
7             =head1 VERSION
8              
9             0.03
10              
11             =head1 DESCRIPTION
12              
13             L is a L "plugin" which can
14             handle 401 digest auth responses from the server.
15              
16             See L.
17              
18             =head1 SYNOPSIS
19              
20             use Mojo::UserAgent::DigestAuth;
21             my $ua = Mojo::UserAgent->new;
22              
23             # blocking
24             $tx = $ua->$_request_with_digest_auth($method, $url, $headers);
25              
26             # non-blocking
27             $ua = $ua->$_request_with_digest_auth($method, $url, $headers, $cb);
28             $ua = $ua->$_request_with_digest_auth($method, $url, $cb);
29              
30             $ua = $ua->$_request_with_digest_auth(
31             get => "http://example.com", sub {
32             my ($ua, $tx) = @_;
33             }
34             );
35              
36             A custom client nonce can be specified by using a special "D-Client-Nonce"
37             header. This is a hack to work around servers which does not understand the
38             nonce generated by this module.
39              
40             Note that this feature is EXPERIMENTAL and might be removed once I figure
41             out why the random nonce L
42             for all servers.
43              
44             $ua = $ua->$_request_with_digest_auth(
45             get => { "D-Client-Nonce" => "0e163838ccd62299" },
46             "http://example.com", sub {
47             my ($ua, $tx) = @_;
48             }
49             );
50              
51             =cut
52              
53 2     2   291876 use Mojo::Base 'Exporter';
  2         4  
  2         10  
54 2     2   845 use Mojo::UserAgent;
  2         241219  
  2         20  
55 2     2   68 use Mojo::Util 'md5_sum';
  2         4  
  2         140  
56 2   50 2   11 use constant DEBUG => $ENV{MOJO_USERAGENT_DIGEST_AUTH_DEBUG} || 0;
  2         2  
  2         3032  
57              
58             our $VERSION = '0.03';
59             our @EXPORT = qw( $_request_with_digest_auth );
60             my $NC = 0;
61              
62             our $_request_with_digest_auth = sub {
63             my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
64             my $ua = shift;
65             my @args = @_;
66             my $tx = $ua->build_tx(@args);
67             my $args = {};
68             my $res;
69              
70             @$args{qw(username password)} = split ':', $tx->req->url->userinfo || '';
71              
72             if (my $client_nonce = $tx->req->headers->header('D-Client-Nonce')) {
73             $args->{client_nonce} = $client_nonce;
74             _clean_tx($tx);
75             }
76              
77             $cb ||= sub { $res = $_[1] };
78             $tx->req->url($tx->req->url->clone)->url->userinfo(undef);
79             warn "[DigestAuth] url=@{[$tx->req->url]}\n" if DEBUG;
80              
81             Mojo::IOLoop->delay(
82             sub { $ua->start($tx, shift->begin) },
83             sub {
84             my ($delay, $tx) = @_;
85             my $code = $tx->res->code || '';
86             warn "[DigestAuth] code=$code\n" if DEBUG;
87             return $ua->$cb($tx) unless 3 == grep { defined $_ } @$args{qw(username password)}, $tx->res->headers->header('WWW-Authenticate');
88             warn "[DigestAuth] Digest authorization...\n" if DEBUG;
89             my $next_tx = _clean_tx($ua->build_tx(@args));
90             $next_tx->req->headers->authorization(sprintf 'Digest %s', join ', ', _digest_kv($tx, $args));
91             $next_tx->req->headers->accept('*/*');
92             $ua->start($next_tx, $delay->begin);
93             },
94             sub { $ua->$cb($_[1]) },
95             )->wait;
96              
97             return $res if $res;
98             return $ua;
99             };
100              
101             sub _clean_tx {
102 3     3   474 my $tx = shift;
103 3         8 $tx->req->headers->remove('D-Client-Nonce');
104 3         36 $tx;
105             }
106              
107             sub _digest_kv {
108 2     2   17 my ($tx, $args) = @_;
109 2         5 my %auth_param = $tx->res->headers->header('WWW-Authenticate') =~ /(\w+)="?([^",]+)"?/g;
110 2         58 my $nc = sprintf '%08X', ++$NC;
111 2         3 my ($ha1, $ha2, $response);
112              
113 2   66     12 $auth_param{client_nonce} = $args->{client_nonce} // _generate_nonce(time);
114 2   50     7 $auth_param{nonce} //= '__UNDEF__';
115 2   50     5 $auth_param{realm} //= '';
116              
117 2         11 $ha1 = _ha1(\%auth_param, @$args{qw( username password )});
118 2         16 $ha2 = _ha2(\%auth_param, $tx->req);
119              
120 2 50 33     19 if ($auth_param{qop} and $auth_param{qop} =~ /^auth/) {
121 2         10 $response = md5_sum join ':', $ha1, $auth_param{nonce}, $nc, $auth_param{client_nonce}, $auth_param{qop}, $ha2;
122 2         8 warn "RESPONSE: MD5($ha1:$auth_param{nonce}:$nc:$auth_param{client_nonce}:$auth_param{qop}:$ha2) = $response\n"
123             if DEBUG;
124             }
125             else {
126 0         0 $response = md5_sum join ':', $ha1, $auth_param{nonce}, $ha2;
127 0         0 warn "RESPONSE: MD5($ha1:$auth_param{nonce}:$ha2) = $response\n" if DEBUG;
128             }
129              
130             return (
131 2 50       10 qq(username="$args->{username}"), qq(realm="$auth_param{realm}"),
    50          
132 2         6 qq(nonce="$auth_param{nonce}"), qq(uri="@{[$tx->req->url->path]}"),
133             $auth_param{qop} ? ("qop=$auth_param{qop}") : (), "nc=$nc",
134             qq(cnonce="$auth_param{client_nonce}"), qq(response="$response"),
135             $auth_param{opaque} ? (qq(opaque="$auth_param{opaque}")) : (), qq(algorithm="MD5"),
136             );
137             }
138              
139             sub _generate_nonce {
140 1     1   1 my $time = shift;
141 1         4 my $nonce = Mojo::Util::b64_encode(join ' ', $time, Mojo::Util::hmac_sha1_sum($time), '');
142 1         35 chomp $nonce;
143 1         4 $nonce =~ s!=+$!!;
144 1         5 return $nonce;
145             }
146              
147             sub _ha1 {
148 2     2   3 my ($auth_param, $username, $password) = @_;
149 2         3 my $res;
150              
151 2 50 33     9 if (!$auth_param->{algorithm} or $auth_param->{algorithm} eq 'MD5') {
152 2         9 $res = md5_sum join ':', $username, $auth_param->{realm}, $password;
153 2         11 warn "HA1: MD5($username:$auth_param->{realm}:$password) = $res\n" if DEBUG;
154             }
155             else {
156 0         0 $res = md5_sum md5_sum(join ':', $username, $auth_param->{realm}, $password), $auth_param->{nonce},
157             $auth_param->{client_nonce};
158 0         0 warn
159             "HA1: MD5(MD5($username:$auth_param->{realm}:$password), $auth_param->{nonce}, $auth_param->{client_nonce}) = $res\n"
160             if DEBUG;
161             }
162              
163 2         25 return $res;
164             }
165              
166             sub _ha2 {
167 2     2   13 my ($auth_param, $req) = @_;
168 2         6 my $method = uc $req->method;
169 2         10 my $res;
170              
171 2 50 33     12 if (!$auth_param->{qop} or $auth_param->{qop} eq 'auth') {
172 2         5 $res = md5_sum join ':', $method, $req->url->path;
173 2         72 warn "HA2: MD5($method:@{[$req->url->path]}) = $res\n" if DEBUG;
174             }
175             else {
176 0         0 $res = md5_sum join ':', $method, $req->url->path, md5_sum('entityBody'); # TODO: entityBody
177 0         0 warn "HA2: MD5(TODO) = $res\n" if DEBUG;
178             }
179              
180 2         4 return $res;
181             }
182              
183             =head1 COPYRIGHT AND LICENSE
184              
185             Copyright (C) 2014, Jan Henning Thorsen
186              
187             This program is free software, you can redistribute it and/or modify it under
188             the terms of the Artistic License version 2.0.
189              
190             =head1 AUTHOR
191              
192             Jan Henning Thorsen - C
193              
194             =cut
195              
196             1;