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