1# -*- Mode: cperl; coding: utf-8; cperl-indent-level: 4 -*-
2# vim: ts=4 sts=4 sw=4:
3package CPAN::HTTP::Client;
4use strict;
5use vars qw(@ISA);
6use CPAN::HTTP::Credentials;
7use HTTP::Tiny 0.005;
8
9$CPAN::HTTP::Client::VERSION = $CPAN::HTTP::Client::VERSION = "1.9602";
10
11# CPAN::HTTP::Client is adapted from parts of cpanm by Tatsuhiko Miyagawa
12# and parts of LWP by Gisle Aas
13
14sub new {
15    my $class = shift;
16    my %args = @_;
17    for my $k ( keys %args ) {
18        $args{$k} = '' unless defined $args{$k};
19    }
20    $args{no_proxy} = [split(",", $args{no_proxy}) ] if $args{no_proxy};
21    return bless \%args, $class;
22}
23
24# This executes a request with redirection (up to 5) and returns the
25# response structure generated by HTTP::Tiny
26#
27# If authentication fails, it will attempt to get new authentication
28# information and repeat up to 5 times
29
30sub mirror {
31    my($self, $uri, $path) = @_;
32
33    my $want_proxy = $self->_want_proxy($uri);
34    my $http = HTTP::Tiny->new(
35        verify_SSL => 1,
36        $want_proxy ? (proxy => $self->{proxy}) : ()
37    );
38
39    my ($response, %headers);
40    my $retries = 0;
41    while ( $retries++ < 5 ) {
42        $response = $http->mirror( $uri, $path, {headers => \%headers} );
43        if ( $response->{status} eq '401' ) {
44            last unless $self->_get_auth_params( $response, 'non_proxy' );
45        }
46        elsif ( $response->{status} eq '407' ) {
47            last unless $self->_get_auth_params( $response, 'proxy' );
48        }
49        else {
50            last; # either success or failure
51        }
52        my %headers = (
53            $self->_auth_headers( $uri, 'non_proxy' ),
54            ( $want_proxy ? $self->_auth_headers($uri, 'proxy') : () ),
55        );
56    }
57
58    return $response;
59}
60
61sub _want_proxy {
62    my ($self, $uri) = @_;
63    return unless $self->{proxy};
64    my($host) = $uri =~ m|://([^/:]+)|;
65    return ! grep { $host =~ /\Q$_\E$/ } @{ $self->{no_proxy} || [] };
66}
67
68# Generates the authentication headers for a given mode
69# C<mode> is 'proxy' or 'non_proxy'
70# C<_${mode}_type> is 'basic' or 'digest'
71# C<_${mode}_params> will be the challenge parameters from the 401/407 headers
72sub _auth_headers {
73    my ($self, $uri, $mode) = @_;
74    # Get names for our mode-specific attributes
75    my ($type_key, $param_key) = map {"_" . $mode . $_} qw/_type _params/;
76
77    # If _prepare_auth has not been called, we can't prepare headers
78    return unless $self->{$type_key};
79
80    # Get user credentials for mode
81    my $cred_method = "get_" . ($mode ? "proxy" : "non_proxy") ."_credentials";
82    my ($user, $pass) = CPAN::HTTP::Credentials->$cred_method;
83
84    # Generate the header for the mode & type
85    my $header = $mode eq 'proxy' ? 'Proxy-Authorization' : 'Authorization';
86    my $value_method = "_" . $self->{$type_key} . "_auth";
87    my $value = $self->$value_method($user, $pass, $self->{$param_key}, $uri);
88
89    # If we didn't get a value, we didn't have the right modules available
90    return $value ? ( $header, $value ) : ();
91}
92
93# Extract authentication parameters from headers, but clear any prior
94# credentials if we failed (so we might prompt user for password again)
95sub _get_auth_params {
96    my ($self, $response, $mode) = @_;
97    my $prefix = $mode eq 'proxy' ? 'Proxy' : 'WWW';
98    my ($type_key, $param_key) = map {"_" . $mode . $_} qw/_type _params/;
99    if ( ! $response->{success} ) { # auth failed
100        my $method = "clear_${mode}_credentials";
101        CPAN::HTTP::Credentials->$method;
102        delete $self->{$_} for $type_key, $param_key;
103    }
104    ($self->{$type_key}, $self->{$param_key}) =
105        $self->_get_challenge( $response, "${prefix}-Authenticate");
106    return $self->{$type_key};
107}
108
109# Extract challenge type and parameters for a challenge list
110sub _get_challenge {
111    my ($self, $response, $auth_header) = @_;
112
113    my $auth_list = $response->{headers}(lc $auth_header);
114    return unless defined $auth_list;
115    $auth_list = [$auth_list] unless ref $auth_list;
116
117    for my $challenge (@$auth_list) {
118        $challenge =~ tr/,/;/;  # "," is used to separate auth-params!!
119        ($challenge) = $self->split_header_words($challenge);
120        my $scheme = shift(@$challenge);
121        shift(@$challenge); # no value
122        $challenge = { @$challenge };  # make rest into a hash
123
124        unless ($scheme =~ /^(basic|digest)$/) {
125            next; # bad scheme
126        }
127        $scheme = $1;  # untainted now
128
129        return ($scheme, $challenge);
130    }
131    return;
132}
133
134# Generate a basic authentication header value
135sub _basic_auth {
136    my ($self, $user, $pass) = @_;
137    unless ( $CPAN::META->has_usable('MIME::Base64') ) {
138        $CPAN::Frontend->mywarn(
139            "MIME::Base64 is required for 'Basic' style authentication"
140        );
141        return;
142    }
143    return "Basic " . MIME::Base64::encode_base64("$user\:$pass", q{});
144}
145
146# Generate a digest authentication header value
147sub _digest_auth {
148    my ($self, $user, $pass, $auth_param, $uri) = @_;
149    unless ( $CPAN::META->has_usable('Digest::MD5') ) {
150        $CPAN::Frontend->mywarn(
151            "Digest::MD5 is required for 'Digest' style authentication"
152        );
153        return;
154    }
155
156    my $nc = sprintf "%08X", ++$self->{_nonce_count}{$auth_param->{nonce}};
157    my $cnonce = sprintf "%8x", time;
158
159    my ($path) = $uri =~ m{^\w+?://[^/]+(/.*)$};
160    $path = "/" unless defined $path;
161
162    my $md5 = Digest::MD5->new;
163
164    my(@digest);
165    $md5->add(join(":", $user, $auth_param->{realm}, $pass));
166    push(@digest, $md5->hexdigest);
167    $md5->reset;
168
169    push(@digest, $auth_param->{nonce});
170
171    if ($auth_param->{qop}) {
172        push(@digest, $nc, $cnonce, ($auth_param->{qop} =~ m|^auth[,;]auth-int$|) ? 'auth' : $auth_param->{qop});
173    }
174
175    $md5->add(join(":", 'GET', $path));
176    push(@digest, $md5->hexdigest);
177    $md5->reset;
178
179    $md5->add(join(":", @digest));
180    my($digest) = $md5->hexdigest;
181    $md5->reset;
182
183    my %resp = map { $_ => $auth_param->{$_} } qw(realm nonce opaque);
184    @resp{qw(username uri response algorithm)} = ($user, $path, $digest, "MD5");
185
186    if (($auth_param->{qop} || "") =~ m|^auth([,;]auth-int)?$|) {
187        @resp{qw(qop cnonce nc)} = ("auth", $cnonce, $nc);
188    }
189
190    my(@order) =
191        qw(username realm qop algorithm uri nonce nc cnonce response opaque);
192    my @pairs;
193    for (@order) {
194        next unless defined $resp{$_};
195        push(@pairs, "$_=" . qq("$resp{$_}"));
196    }
197
198    my $auth_value  = "Digest " . join(", ", @pairs);
199    return $auth_value;
200}
201
202# split_header_words adapted from HTTP::Headers::Util
203sub split_header_words {
204    my ($self, @words) = @_;
205    my @res = $self->_split_header_words(@words);
206    for my $arr (@res) {
207        for (my $i = @$arr - 2; $i >= 0; $i -= 2) {
208            $arr->[$i] = lc($arr->[$i]);
209        }
210    }
211    return @res;
212}
213
214sub _split_header_words {
215    my($self, @val) = @_;
216    my @res;
217    for (@val) {
218        my @cur;
219        while (length) {
220            if (s/^\s*(=*[^\s=;,]+)//) {  # 'token' or parameter 'attribute'
221                push(@cur, $1);
222                # a quoted value
223                if (s/^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"//) {
224                    my $val = $1;
225                    $val =~ s/\\(.)/$1/g;
226                    push(@cur, $val);
227                    # some unquoted value
228                }
229                elsif (s/^\s*=\s*([^;,\s]*)//) {
230                    my $val = $1;
231                    $val =~ s/\s+$//;
232                    push(@cur, $val);
233                    # no value, a lone token
234                }
235                else {
236                    push(@cur, undef);
237                }
238            }
239            elsif (s/^\s*,//) {
240                push(@res, [@cur]) if @cur;
241                @cur = ();
242            }
243            elsif (s/^\s*;// || s/^\s+//) {
244                # continue
245            }
246            else {
247                die "This should not happen: '$_'";
248            }
249        }
250        push(@res, \@cur) if @cur;
251    }
252    @res;
253}
254
2551;
256