File Coverage

blib/lib/App/SpamcupNG/UserAgent.pm
Criterion Covered Total %
statement 51 131 38.9
branch 4 42 9.5
condition 0 5 0.0
subroutine 14 19 73.6
pod 6 6 100.0
total 75 203 36.9


line stmt bran cond sub pod time code
1             package App::SpamcupNG::UserAgent;
2 6     6   131547 use warnings;
  6         19  
  6         198  
3 6     6   32 use strict;
  6         11  
  6         155  
4 6     6   31 use Carp qw(croak);
  6         12  
  6         355  
5 6     6   4406 use LWP::UserAgent 6.60;
  6         273866  
  6         253  
6 6     6   142 use HTTP::Request 6.36;
  6         131  
  6         248  
7 6     6   895 use Log::Log4perl 1.54 qw(get_logger :levels);
  6         45535  
  6         77  
8 6     6   3915 use HTTP::CookieJar::LWP 0.012;
  6         175667  
  6         260  
9 6     6   52 use Mozilla::PublicSuffix v1.0.6;
  6         93  
  6         372  
10 6     6   3546 use HTTP::Request::Common qw(POST);
  6         13403  
  6         8764  
11              
12             our $VERSION = '0.017'; # VERSION
13              
14             =head1 NAME
15              
16             App::SpamcupNG::UserAgent - the SpamcupNG HTTP user agent
17              
18             =head1 SYNOPSIS
19              
20             =head1 DESCRIPTION
21              
22             This class is responsible to interact with the Spamcop website, providing
23             requests and returning the HTML responses.
24              
25             =head1 METHODS
26              
27             =head2 new
28              
29             Creates a new instance.
30              
31             Expects as parameter:
32              
33             - version: a string of the version of SpamcupNG.
34              
35             Returns a new instance.
36              
37             =cut
38              
39             sub new {
40 2     2 1 1022 my ( $class, $version ) = @_;
41 2 100       24 die 'The parameter version is required' unless ($version);
42              
43 1         11 my $self = {
44             name => 'SpamcupNG user agent',
45             version => $version,
46             members_url => 'https://members.spamcop.net/',
47             code_login_url => 'https://www.spamcop.net/?code=',
48             report_url => 'https://www.spamcop.net/sc?id=',
49             form_login_url => 'https://www.spamcop.net/mcgi',
50             domain => 'https://www.spamcop.net/',
51             password_field => 'password',
52             current_base_url => undef
53             };
54              
55 1         2 bless $self, $class;
56              
57             my $ua = LWP::UserAgent->new(
58 1         17 agent => ( $self->{name} . '/' . $version ),
59             protocols_allowed => ['https'],
60             cookie_jar => HTTP::CookieJar::LWP->new
61             );
62              
63             # for form based authentication
64 1         3106 push @{ $ua->requests_redirectable }, 'POST';
  1         6  
65 1         14 $self->{user_agent} = $ua;
66 1         8 return $self;
67             }
68              
69             =head2 user_agent
70              
71             Returns a string with the HTTP header user-agent that will be used by the inner
72             HTTP user agent.
73              
74             =cut
75              
76             sub user_agent {
77 1     1 1 2 my $self = shift;
78 1         4 return $self->{user_agent}->agent;
79             }
80              
81             =head2 login
82              
83             Execute the login to Spamcop website.
84              
85             If form based authentication is in use, it will login just once and return the
86             response of HTTP GET to Spamcop root URL.
87              
88             Expect as parameters:
89              
90             =over
91              
92             =item *
93              
94             id: the ID of a Spamcop account.
95              
96             =item *
97              
98             password: the password of a Spamcop account.
99              
100             =back
101              
102             Returns the HTTP response (HTML content) as a scalar reference.
103              
104             =cut
105              
106             # copied from HTTP::Request::as_string
107             sub _request_line {
108 0     0   0 my $request = shift;
109 0   0     0 my $req_line = $request->method || "-";
110 0         0 my $uri = $request->uri;
111 0 0       0 $uri = ( defined $uri ) ? $uri->as_string : "-";
112 0         0 $req_line .= " $uri";
113 0         0 my $proto = $request->protocol;
114 0 0       0 $req_line .= " $proto" if $proto;
115 0         0 return $req_line;
116             }
117              
118             sub _redact_auth_req {
119 1     1   10129 my ( $self, $request ) = @_;
120 1         3 my @lines;
121              
122 1 50       3 return $request->as_string if ( $self->_is_authenticated );
123              
124 1 50       94 if ( $request->method eq 'POST' ) {
125 0         0 push( @lines, _request_line($request) );
126 0         0 push( @lines, $request->headers_as_string );
127 0         0 my @params = split( '&', $request->content );
128             my %params
129 0         0 = map { my @tmp = split( '=', $_ ); $tmp[0] => $tmp[1] } @params;
  0         0  
  0         0  
130             croak( 'Unexpected request content, missing '
131             . $self->{password_field}
132             . ' field' )
133 0 0       0 unless exists( $params{ $self->{password_field} } );
134 0         0 my $redacted = '*' x length( $params{ $self->{password_field} } );
135 0         0 $params{ $self->{password_field} } = $redacted;
136              
137 0         0 while ( my ( $key, $value ) = each %params ) {
138 0         0 push( @lines, "$key=$value" );
139             }
140             }
141             else {
142 1         20 @lines = split( "\n", $request->as_string );
143 1         191 my $secret = ( split( /\s/, $lines[1] ) )[2];
144 1         6 my $redacted = '*' x length($secret);
145 1         17 $lines[1] =~ s/$secret/$redacted/;
146             }
147              
148 1         8 return join( "\n", @lines );
149             }
150              
151             sub _dump_cookies {
152 0     0   0 my $self = shift;
153             my @cookies = $self->{user_agent}
154 0         0 ->cookie_jar->dump_cookies( { persistent => 1 } );
155 0         0 my $counter = 0;
156 0         0 my @dump;
157              
158 0         0 foreach my $cookie (@cookies) {
159 0         0 push( @dump, ( $counter . ' => ' . $cookie ) );
160             }
161              
162 0         0 return join( "\n", @dump );
163             }
164              
165             sub _is_authenticated {
166 1     1   2 my $self = shift;
167 1         5 return $self->{user_agent}->cookie_jar->cookies_for( $self->{domain} );
168             }
169              
170             sub login {
171 0     0 1 0 my ( $self, $id, $password, $is_basic ) = @_;
172 0 0       0 $is_basic = 0 unless ( defined($is_basic) );
173 0         0 my $logger = get_logger('SpamcupNG');
174 0         0 my $request;
175              
176 0 0       0 if ( $logger->is_debug ) {
177 0         0 $logger->debug( "Initial cookies:\n" . $self->_dump_cookies );
178             }
179              
180 0 0       0 if ( $self->_is_authenticated ) {
181 0         0 $logger->info('Already authenticated');
182 0         0 $request = HTTP::Request->new( GET => $self->{domain} );
183             }
184             else {
185 0 0       0 if ($password) {
186              
187 0 0       0 if ($is_basic) {
188 0         0 $request = HTTP::Request->new( GET => $self->{members_url} );
189 0         0 $request->authorization_basic( $id, $password );
190             }
191             else {
192             $request = POST $self->{form_login_url},
193             [
194             username => $id,
195 0         0 $self->{password_field} => $password,
196             duration => '+12h',
197             action => 'cookielogin',
198             returnurl => '/'
199             ];
200             }
201             }
202             else {
203             $request
204 0         0 = HTTP::Request->new( GET => $self->{code_login_url} . $id );
205             }
206             }
207              
208 0         0 $request->protocol('HTTP/1.1');
209              
210 0 0       0 if ( $logger->is_debug() ) {
211 0         0 $logger->debug(
212             "Request details:\n" . ( $self->_redact_auth_req($request) ) );
213             }
214              
215 0         0 my $response = $self->{user_agent}->request($request);
216              
217 0 0       0 if ( $logger->is_debug() ) {
218 0         0 $logger->debug( "Got response:\n" . $response->as_string );
219 0         0 $logger->debug(
220             "After authentication cookies:\n" . $self->_dump_cookies );
221             }
222              
223 0 0       0 return \( $response->content ) if ( $response->is_success );
224              
225 0         0 my $status = $response->status_line();
226              
227 0 0       0 if ( $response->code() == 500 ) {
228 0         0 $logger->fatal("Can\'t connect to server: $status");
229             }
230             else {
231 0         0 $logger->warn($status);
232              
233 0 0 0     0 if ( ($password) and ( $is_basic == 0 ) ) {
234 0         0 $logger->warn('Retrying with basic authentication');
235 0         0 return $self->login( $id, $password, 1 );
236             }
237              
238             $logger->fatal(
239 0         0 'Cannot connect to server or invalid credentials. Please verify your username and password and try again.'
240             );
241             }
242              
243 0         0 return undef;
244             }
245              
246             =head2 spam_report
247              
248             Fetches a SPAM report.
249              
250             Expects as parameter a report ID.
251              
252             Returns the HTML content as a scalar reference.
253              
254             =cut
255              
256             sub spam_report {
257 0     0 1 0 my ( $self, $report_id ) = @_;
258 0         0 my $logger = get_logger('SpamcupNG');
259             my $request
260 0         0 = HTTP::Request->new( GET => $self->{report_url} . $report_id );
261              
262 0 0       0 if ( $logger->is_debug ) {
263 0         0 $logger->debug( "Request to be sent:\n" . $request->as_string );
264             }
265              
266 0         0 my $response = $self->{user_agent}->request($request);
267 0         0 $self->{current_base_url} = $response->base;
268              
269 0 0       0 if ( $logger->is_debug ) {
270 0         0 $logger->debug( "Got HTTP response:\n" . $response->as_string );
271             }
272              
273 0 0       0 unless ( $response->is_success ) {
274 0         0 $logger->fatal("Can't connect to server. Try again later.");
275 0         0 return undef;
276             }
277              
278 0         0 return \( $response->content );
279             }
280              
281             =head2 base
282              
283             Returns the current base URL provided by the last response of getting a SPAM
284             report.
285              
286             =cut
287              
288             sub base {
289 1     1 1 3588 my $self = shift;
290 1         6 return $self->{current_base_url};
291             }
292              
293             =head2 complete_report
294              
295             Complete the SPAM report, by confirming it's information is OK.
296              
297             Returns the HTML content as a scalar reference.
298              
299             =cut
300              
301             sub complete_report {
302 0     0 1   my ( $self, $http_request ) = @_;
303 0           my $logger = get_logger('SpamcupNG');
304 0           my $response = $self->{user_agent}->request($http_request);
305              
306 0 0         if ( $logger->is_debug ) {
307 0           $logger->debug( "Got HTTP response:\n" . $response->as_string );
308             }
309              
310 0 0         unless ( $response->is_success ) {
311 0           $logger->fatal(
312             'Cannot connect to server. Try again later. Quitting.');
313 0           return undef;
314             }
315              
316 0           return \( $response->content );
317             }
318              
319             =head1 AUTHOR
320              
321             Alceu Rodrigues de Freitas Junior, E<lt>arfreitas@cpan.orgE<gt>
322              
323             =head1 COPYRIGHT AND LICENSE
324              
325             This software is copyright (c) 2018 of Alceu Rodrigues de Freitas Junior,
326             E<lt>arfreitas@cpan.orgE<gt>
327              
328             This file is part of App-SpamcupNG distribution.
329              
330             App-SpamcupNG is free software: you can redistribute it and/or modify it under
331             the terms of the GNU General Public License as published by the Free Software
332             Foundation, either version 3 of the License, or (at your option) any later
333             version.
334              
335             App-SpamcupNG is distributed in the hope that it will be useful, but WITHOUT
336             ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
337             FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
338              
339             You should have received a copy of the GNU General Public License along with
340             App-SpamcupNG. If not, see <http://www.gnu.org/licenses/>.
341              
342             =cut
343              
344             1;