App-SpamcupNG

 view release on metacpan or  search on metacpan

lib/App/SpamcupNG/UserAgent.pm  view on Meta::CPAN

package App::SpamcupNG::UserAgent;
use warnings;
use strict;
use Carp qw(confess);
use LWP::UserAgent 6.72;
use HTTP::Request 6.44;
use Log::Log4perl 1.57 qw(get_logger :levels);
use HTTP::CookieJar::LWP 0.014;
use Mozilla::PublicSuffix v1.0.6;
use HTTP::Request::Common 6.44 qw(POST);

our $VERSION = '0.020'; # VERSION

=head1 NAME

App::SpamcupNG::UserAgent - the SpamcupNG HTTP user agent

=head1 SYNOPSIS

=head1 DESCRIPTION

This class is responsible to interact with the Spamcop website, providing
requests and returning the HTML responses.

=head1 METHODS

=head2 new

Creates a new instance.

Expects as parameter:

- version: a string of the version of SpamcupNG.

Returns a new instance.

=cut

sub new {
    my ( $class, $version ) = @_;
    confess 'The parameter version is required' unless ($version);

    my $self = {
        name             => 'SpamcupNG user agent',
        version          => $version,
        members_url      => 'https://members.spamcop.net/',
        code_login_url   => 'https://www.spamcop.net/?code=',
        report_url       => 'https://www.spamcop.net/sc?id=',
        form_login_url   => 'https://www.spamcop.net/mcgi',
        domain           => 'https://www.spamcop.net/',
        password_field   => 'password',
        current_base_url => undef
    };

    bless $self, $class;

    my $ua = LWP::UserAgent->new(
        agent             => ( $self->{name} . '/' . $version ),
        protocols_allowed => ['https'],
        cookie_jar        => HTTP::CookieJar::LWP->new
    );

    # for form based authentication
    push @{ $ua->requests_redirectable }, 'POST';
    $self->{user_agent} = $ua;
    return $self;
}

=head2 user_agent

Returns a string with the HTTP header user-agent that will be used by the inner
HTTP user agent.

=cut

sub user_agent {
    my $self = shift;
    return $self->{user_agent}->agent;
}

=head2 login

Execute the login to Spamcop website.

If form based authentication is in use, it will login just once and return the
response of HTTP GET to Spamcop root URL.

Expect as parameters:

=over

=item *

id: the ID of a Spamcop account.

=item *

password: the password of a Spamcop account.

=back

Returns the HTTP response (HTML content) as a scalar reference.

=cut

# copied from HTTP::Request::as_string
sub _request_line {
    my $request  = shift;
    my $req_line = $request->method || "-";
    my $uri      = $request->uri;
    $uri = ( defined $uri ) ? $uri->as_string : "-";
    $req_line .= " $uri";
    my $proto = $request->protocol;
    $req_line .= " $proto" if $proto;
    return $req_line;
}

sub _redact_auth_req {
    my ( $self, $request ) = @_;
    my @lines;

    return $request->as_string if ( $self->_is_authenticated );

    if ( $request->method eq 'POST' ) {
        push( @lines, _request_line($request) );
        push( @lines, $request->headers_as_string );
        my @params = split( '&', $request->content );
        my %params =
          map { my @tmp = split( '=', $_ ); $tmp[0] => $tmp[1] } @params;
        confess('Unexpected request content, missing '
              . $self->{password_field}
              . ' field' )
          unless exists( $params{ $self->{password_field} } );
        my $redacted = '*' x length( $params{ $self->{password_field} } );
        $params{ $self->{password_field} } = $redacted;

        while ( my ( $key, $value ) = each %params ) {
            push( @lines, "$key=$value" );
        }
    }
    else {
        @lines = split( "\n", $request->as_string );
        my $secret   = ( split( /\s/, $lines[1] ) )[2];
        my $redacted = '*' x length($secret);
        $lines[1] =~ s/$secret/$redacted/;
    }

    return join( "\n", @lines );
}

sub _dump_cookies {
    my $self = shift;
    my @cookies =
      $self->{user_agent}->cookie_jar->dump_cookies( { persistent => 1 } );
    my $counter = 0;
    my @dump;

    foreach my $cookie (@cookies) {
        push( @dump, ( $counter . ' => ' . $cookie ) );
    }

    return join( "\n", @dump );
}

sub _is_authenticated {
    my $self = shift;
    return $self->{user_agent}->cookie_jar->cookies_for( $self->{domain} );
}

sub login {
    my ( $self, $id, $password, $is_basic ) = @_;
    $is_basic = 0 unless ( defined($is_basic) );
    my $logger = get_logger('SpamcupNG');
    my $request;

    if ( $logger->is_debug ) {
        $logger->debug( "Initial cookies:\n" . $self->_dump_cookies );
    }

    if ( $self->_is_authenticated ) {
        $logger->debug('Already authenticated');
        $request = HTTP::Request->new( GET => $self->{domain} );
    }
    else {
        if ($password) {

            if ($is_basic) {
                $request = HTTP::Request->new( GET => $self->{members_url} );
                $request->authorization_basic( $id, $password );
            }
            else {
                $request = POST $self->{form_login_url},
                  [
                    username                => $id,
                    $self->{password_field} => $password,
                    duration                => '+12h',
                    action                  => 'cookielogin',
                    returnurl               => '/'
                  ];
            }
        }
        else {
            $request =
              HTTP::Request->new( GET => $self->{code_login_url} . $id );
        }
    }

    $request->protocol('HTTP/1.1');

    if ( $logger->is_debug() ) {
        $logger->debug(
            "Request details:\n" . ( $self->_redact_auth_req($request) ) );
    }

    my $response = $self->{user_agent}->request($request);

    if ( $logger->is_debug() ) {
        $logger->debug( "Got response:\n" . $response->as_string );
        $logger->debug(
            "After authentication cookies:\n" . $self->_dump_cookies );
    }

    return \( $response->content ) if ( $response->is_success );

    my $status = $response->status_line();

    if ( $response->code() == 500 ) {
        $logger->die("Can\'t connect to server: $status");
    }
    else {
        $logger->warn($status);

        if ( ($password) and ( $is_basic == 0 ) ) {
            $logger->warn('Retrying with basic authentication');
            return $self->login( $id, $password, 1 );
        }

        $logger->die(
'Cannot connect to server or invalid credentials. Please verify your username and password and try again.'
        );
    }

    return undef;
}

=head2 spam_report

Fetches a SPAM report.

Expects as parameter a report ID.

Returns the HTML content as a scalar reference.

=cut

sub spam_report {
    my ( $self, $report_id ) = @_;
    my $logger  = get_logger('SpamcupNG');
    my $request = HTTP::Request->new( GET => $self->{report_url} . $report_id );

    if ( $logger->is_debug ) {
        $logger->debug( "Request to be sent:\n" . $request->as_string );
    }

    my $response = $self->{user_agent}->request($request);
    $self->{current_base_url} = $response->base;

    if ( $logger->is_debug ) {
        $logger->debug( "Got HTTP response:\n" . $response->as_string );
    }

    unless ( $response->is_success ) {
        $logger->die("Can't connect to server. Try again later.");
    }

    return \( $response->content );
}

=head2 base

Returns the current base URL provided by the last response of getting a SPAM
report.

=cut

sub base {
    my $self = shift;
    return $self->{current_base_url};
}

=head2 complete_report

Complete the SPAM report, by confirming it's information is OK.

Returns the HTML content as a scalar reference.

=cut

sub complete_report {
    my ( $self, $http_request ) = @_;
    my $logger   = get_logger('SpamcupNG');
    my $response = $self->{user_agent}->request($http_request);

    if ( $logger->is_debug ) {
        $logger->debug( "Got HTTP response:\n" . $response->as_string );
    }

    unless ( $response->is_success ) {
        $logger->die('Cannot connect to server. Try again later. Quitting.');
    }

    return \( $response->content );
}

=head1 AUTHOR

Alceu Rodrigues de Freitas Junior, E<lt>glasswalk3r@yahoo.com.brE<gt>

=head1 COPYRIGHT AND LICENSE



( run in 0.560 second using v1.01-cache-2.11-cpan-acf6aa7dc9e )