File Coverage

blib/lib/Armadito/Agent/HTTP/Client.pm
Criterion Covered Total %
statement 21 76 27.6
branch 0 38 0.0
condition 0 26 0.0
subroutine 7 10 70.0
pod 2 2 100.0
total 30 152 19.7


line stmt bran cond sub pod time code
1             package Armadito::Agent::HTTP::Client;
2              
3 25     25   3559990 use strict;
  25         37  
  25         652  
4 25     25   93 use warnings;
  25         26  
  25         711  
5              
6 25     25   87 use English qw(-no_match_vars);
  25         61  
  25         158  
7 25     25   20849 use HTTP::Status;
  25         77322  
  25         5600  
8 25     25   15608 use LWP::UserAgent;
  25         672891  
  25         254  
9 25     25   808 use UNIVERSAL::require;
  25         32  
  25         199  
10              
11 25     25   2188 use Armadito::Agent::Logger qw(LOG_DEBUG2);
  25         37  
  25         15826  
12              
13             my $log_prefix = "[http client] ";
14              
15             sub new {
16 0     0 1   my ( $class, %params ) = @_;
17              
18             die "non-existing certificate file $params{ca_cert_file}"
19 0 0 0       if $params{ca_cert_file} && !-f $params{ca_cert_file};
20              
21             die "non-existing certificate directory $params{ca_cert_dir}"
22 0 0 0       if $params{ca_cert_dir} && !-d $params{ca_cert_dir};
23              
24             my $self = {
25             logger => $params{logger} || Armadito::Agent::Logger->new(),
26             user => $params{user},
27             password => $params{password},
28             ssl_set => 0,
29             no_ssl_check => $params{no_ssl_check},
30             ca_cert_dir => $params{ca_cert_dir},
31             ca_cert_file => $params{ca_cert_file}
32 0   0       };
33 0           bless $self, $class;
34              
35             $self->{ua} = LWP::UserAgent->new(
36             requests_redirectable => [ 'POST', 'GET', 'HEAD' ],
37             agent => $Armadito::Agent::AGENT_STRING,
38 0   0       timeout => $params{timeout} || 180,
39             parse_head => 0,
40             keep_alive => 1,
41             );
42              
43 0 0         if ( $params{proxy} ) {
44 0           $self->{ua}->proxy( [ 'http', 'https' ], $params{proxy} );
45             }
46             else {
47 0           $self->{ua}->env_proxy();
48             }
49              
50 0           return $self;
51             }
52              
53             sub request {
54 0     0 1   my ( $self, $request, $file ) = @_;
55              
56 0           my $logger = $self->{logger};
57              
58 0           my $url = $request->uri();
59 0           my $scheme = $url->scheme();
60 0 0 0       $self->_setSSLOptions() if $scheme eq 'https' && !$self->{ssl_set};
61              
62 0           my $result = HTTP::Response->new(500);
63 0           eval {
64 0 0 0       if ( $OSNAME eq 'MSWin32' && $scheme eq 'https' ) {
65 0           alarm $self->{ua}->timeout();
66             }
67 0           $result = $self->{ua}->request( $request, $file );
68 0           alarm 0;
69             };
70              
71 0 0         if ( !$result->is_success() ) {
72              
73             # authentication required
74 0 0         if ( $result->code() == 401 ) {
75 0 0 0       if ( $self->{user} && $self->{password} ) {
76 0           $logger->debug( $log_prefix . "authentication required, submitting credentials" );
77              
78 0           my $header = $result->header('www-authenticate');
79 0           my ($realm) = $header =~ /^Basic realm="(.*)"/;
80 0           my $host = $url->host();
81 0   0       my $port = $url->port()
82             || ( $scheme eq 'https' ? 443 : 80 );
83 0           $self->{ua}->credentials( "$host:$port", $realm, $self->{user}, $self->{password} );
84              
85             # replay request
86 0           eval {
87 0 0 0       if ( $OSNAME eq 'MSWin32' && $scheme eq 'https' ) {
88 0           alarm $self->{ua}->{timeout};
89             }
90 0           $result = $self->{ua}->request( $request, $file );
91 0           alarm 0;
92             };
93 0 0         if ( !$result->is_success() ) {
94 0           $logger->error( $log_prefix . "authentication required, wrong credentials" );
95             }
96             }
97             else {
98             # abort
99 0           $logger->error( $log_prefix . "authentication required, no credentials available" );
100             }
101             }
102             else {
103 0           $logger->error( $log_prefix . "communication error: " . $result->status_line() );
104             }
105             }
106              
107 0           return $result;
108             }
109              
110             sub _setSSLOptions {
111 0     0     my ($self) = @_;
112              
113 0 0         if ( $self->{no_ssl_check} ) {
114              
115             # LWP 6 default behaviour is to check hostname
116             # Fedora also backported this behaviour change in its LWP5 package, so
117             # just checking on LWP version is not enough
118             $self->{ua}->ssl_opts( verify_hostname => 0, SSL_verify_mode => 0 )
119 0 0         if $self->{ua}->can('ssl_opts');
120             }
121             else {
122             # only IO::Socket::SSL can perform full server certificate validation,
123             # Net::SSL is only able to check certification authority, and not
124             # certificate hostname
125 0           IO::Socket::SSL->require();
126 0 0         die "IO::Socket::SSL Perl module not available, "
127             . "unable to validate SSL certificates "
128             . "(workaround: use 'no-ssl-check' configuration parameter)"
129             if $EVAL_ERROR;
130              
131 0 0         if ( $self->{logger}{verbosity} > LOG_DEBUG2 ) {
132 0           $Net::SSLeay::trace = 2;
133             }
134              
135 0 0         if ( $LWP::VERSION >= 6 ) {
136             $self->{ua}->ssl_opts( SSL_ca_file => $self->{ca_cert_file} )
137 0 0         if $self->{ca_cert_file};
138             $self->{ua}->ssl_opts( SSL_ca_path => $self->{ca_cert_dir} )
139 0 0         if $self->{ca_cert_dir};
140             }
141             else {
142             # SSL_verifycn_scheme and SSL_verifycn_name are required
143 0 0         die "IO::Socket::SSL Perl module too old "
144             . "(available: $IO::Socket::SSL::VERSION, required: 1.14), "
145             . "unable to validate SSL certificates "
146             . "(workaround: use 'no-ssl-check' configuration parameter)"
147             if $IO::Socket::SSL::VERSION < 1.14;
148              
149             # use a custom HTTPS handler to workaround default LWP5 behaviour
150             Armadito::Agent::HTTP::Protocol::https->use(
151             ca_cert_file => $self->{ca_cert_file},
152             ca_cert_dir => $self->{ca_cert_dir},
153 0           );
154              
155 0           LWP::Protocol::implementor( 'https', 'Armadito::Agent::HTTP::Protocol::https' );
156              
157             # abuse user agent internal to pass values to the handler, so
158             # as to have different behaviors in the same process
159 0 0         $self->{ua}->{ssl_check} = $self->{no_ssl_check} ? 0 : 1;
160             }
161             }
162              
163 0           $self->{ssl_set} = 1;
164             }
165              
166             1;
167             __END__
168              
169             =head1 NAME
170              
171             Armadito::Agent::HTTP::Client - An abstract HTTP client
172              
173             =head1 DESCRIPTION
174              
175             This is an abstract class for HTTP clients. It can send messages through HTTP
176             or HTTPS, directly or through a proxy, and validate SSL certificates.
177              
178             =head1 METHODS
179              
180             =head2 new(%params)
181              
182             The constructor. The following parameters are allowed, as keys of the %params
183             hash:
184              
185             =over
186              
187             =item I<logger>
188              
189             the logger object to use (default: a new stderr logger)
190              
191             =item I<proxy>
192              
193             the URL of an HTTP proxy
194              
195             =item I<user>
196              
197             the user for HTTP authentication
198              
199             =item I<password>
200              
201             the password for HTTP authentication
202              
203             =item I<no_ssl_check>
204              
205             a flag allowing to ignore untrusted server certificates (default: false)
206              
207             =item I<ca_cert_file>
208              
209             the file containing trusted certificates
210              
211             =item I<ca_cert_dir>
212              
213             the directory containing trusted certificates
214              
215             =back
216              
217             =head2 request($request)
218              
219             Send given HTTP::Request object, handling SSL checking and user authentication
220             automatically if needed.