The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
use strict;
use warnings;
no warnings "void";

=head1 NAME

LWP::UserAgent::Paranoid - A modern LWPx::ParanoidAgent for safer requests

=cut

package LWP::UserAgent::Paranoid;
use base 'LWP::UserAgent';

our $VERSION = "0.96";

use Scalar::Util          qw/ refaddr /;
use Time::HiRes           qw/ alarm /;
use LWPx::ParanoidHandler qw//;
use Net::DNS::Paranoid    qw//;
use Carp                  qw//;

=head1 SYNOPSIS

    use LWP::UserAgent::Paranoid;
    my $ua = LWP::UserAgent::Paranoid->new(
        request_timeout => 5   # seconds; may be fractional
    );

    # use $ua as a normal LWP::UserAgent...
    my $response = $ua->get("http://example.com");

    # allow requests to localhost and 127.0.0.1
    $ua->resolver->whitelisted_hosts(['localhost', '127.0.0.1']);

=head1 DESCRIPTION

This module is a more modern L<LWPx::ParanoidAgent> with cleaner internals and
a very similar feature set.  It is a not a drop-in replacement, however, since
the API differs.

The primary features provided by this module:

=head2 Overall request timeout

A configurable timeout from start to finish of a "logical" request made by
calling one of L<LWP::UserAgent>'s request methods.  It encompasses all
followed redirects to ensure that you can't be tarpitted by a series of
stalling redirects.  The default is 5 seconds.

=head2 Blocked private hosts and IP address ranges

All new agents are automatically made paranoid of private hostnames and IP
address ranges using L<LWPx::ParanoidHandler>.  You may access the
L<Net::DNS::Paranoid> resolver via the L</resolver> method in order to
customize the blocked or whitelisted hosts.

=head1 EVEN MORE PARANOIA

You may also wish to tune standard L<LWP::UserAgent> parameters for greater
paranoria depending on your requirements:

=head2 Maximum number of redirects

Although generally unnecessary given the request timeout, you can tune
L<LWP::UserAgent/max_redirects> down from the default of 7.

=head2 Protocols/URI schemes allowed

If you don't want to allow requests for schemes other than http and https, you
may use L<LWP::UserAgent/protocols_allowed> either as a method or as an option
to I<new>.

    $ua->protocols_allowed(["http", "https"]);

=head1 WHY NOT LWPx::ParanoidAgent?

L<LWPx::ParanoidAgent>'s implemention involves a 2009-era fork of LWP's http
and https protocol handlers, and it is no longer maintained.  A more
maintainable approach is taken by this module and L<LWPx::ParanoidHandler>.

=head1 METHODS

All methods from L<LWP::UserAgent> are available via inheritence.  In addition,
the following methods are available:

=head2 request_timeout

Gets/sets the timeout which encapsulates each logical request, including any
redirects which are followed.  The default is 5 seconds.  Fractional seconds
are OK.

=head2 resolver

Gets the DNS resolver which is used to block private hosts.  There is little
need to set your own but if you do it should be an L<Net::DNS::Paranoid>
object.  This attribute is read-only, so if you want to replace the resolver
you need to call L</new> again to create a new L<LWP::UserAgent::Paranoid>.

Use the blocking and whitelisting methods on the resolver to customize the
behaviour.

=cut

sub new {
    my ($class, %opts) = @_;

    my $timeout = delete $opts{request_timeout};
       $timeout = 5 unless $timeout;

    my $resolver = delete $opts{resolver};
       $resolver = Net::DNS::Paranoid->new unless $resolver;

    my $self = $class->SUPER::new(%opts);
    $self->request_timeout($timeout);
    $self->_elem("resolver", $resolver);

    LWPx::ParanoidHandler::make_paranoid($self, $self->resolver);

    return $self;
}

sub request_timeout { shift->_elem("request_timeout", @_) }
sub resolver        {
    my $self = shift;
    Carp::croak("resolver is read-only; to use a new resolver, create a new user agent")
        if @_;
    return $self->_elem("resolver");
}

sub __timed_out { Carp::croak("Client timed out request") }
sub __with_timeout {
    my $method  = shift;
    my $self    = shift;
    my $SUPER   = $self->can("SUPER::$method")
        or Carp::croak("No such method '$method'");

    my $our_alarm = (
                ref($SIG{ALRM}) eq "CODE"
        and refaddr($SIG{ALRM}) eq refaddr(\&__timed_out)
    );

    if (not $our_alarm) {
        local $SIG{ALRM} = \&__timed_out;
        alarm $self->request_timeout;
        my $ret = $self->$SUPER(@_);
        alarm 0;
        return $ret;
    } else {
        return $self->$SUPER(@_);
    }
};

sub request        { __with_timeout("request",        @_) }
sub simple_request { __with_timeout("simple_request", @_) }

"The truth is out there.";

=head1 CAVEATS

The overall request timeout is implemented using SIGALRM.  Any C<$SIG{ALRM}>
handler from an outer scope is replaced in the scope of
L<LWP::UserAgent::Paranoid> requests.

=head1 BUGS

All bugs should be reported via
L<rt.cpan.org|https://rt.cpan.org/Public/Dist/Display.html?Name=LWP-UserAgent-Paranoid>
or L<bug-LWP-UserAgent-Paranoid@rt.cpan.org>.

=head1 AUTHOR

Thomas Sibley <tsibley@cpan.org>

=head1 LICENSE AND COPYRIGHT
 
This software is Copyright (c) 2013 by Best Practical Solutions
 
This is free software, licensed under:
 
  The GNU General Public License, Version 2, June 1991

=cut