The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use strict;
use Carp qw(croak);
use URI;
our $VERSION = "0.04";
sub new {
my ($class, %args) = @_;
my $self = bless {
proxy_obj => undef,
no_proxy => []
}, $class;
$self->_env_proxy_for($args{env_proxy});
my $proxy = $args{proxy};
if(defined($proxy)) {
$self->_set_proxy($proxy);
}
my $no_proxy = $args{no_proxy};
if(defined($no_proxy)) {
$self->_set_no_proxy($no_proxy);
}
return $self;
}
sub _set_proxy {
my ($self, $proxy) = @_;
if($proxy eq "") {
$self->{proxy_obj} = undef;
return;
}
my $proxy_uri = URI->new($proxy);
my $scheme = $proxy_uri->scheme;
if(!defined($scheme) || $scheme ne "http") {
croak "Only http proxy is supported: $proxy";
}
$self->{proxy_obj} = AnyEvent::Connector::Proxy::http->new($proxy_uri);
}
sub _set_no_proxy {
my ($self, $no_proxy) = @_;
my $ref = ref($no_proxy);
if($ref eq "ARRAY") {
;
}elsif(!$ref) {
$no_proxy = [$no_proxy];
}else {
croak "no_proxy expects STRING or ARRAYREF, but it was $ref";
}
$self->{no_proxy} = [grep {$_ ne ""} @$no_proxy];
}
sub _env_proxy_for {
my ($self, $protocol) = @_;
return if !defined($protocol);
$self->_env_no_proxy();
my @keys = (lc($protocol) . "_proxy", uc($protocol) . "_PROXY");
foreach my $key (@keys) {
my $p = $ENV{$key};
if(defined($p)) {
$self->_set_proxy($p);
return;
}
}
}
sub _env_no_proxy {
my ($self) = @_;
foreach my $key (qw(no_proxy NO_PROXY)) {
my $no_proxy = $ENV{$key};
if(defined($no_proxy)) {
$self->_set_no_proxy([split /\s*,\s*/, $no_proxy]);
return;
}
}
}
sub _proxy_uri_for {
my ($self, $host, $port) = @_;
foreach my $no_domain (@{$self->{no_proxy}}) {
if($host =~ /\Q$no_domain\E$/) {
return undef;
}
}
return $self->{proxy_obj};
}
sub proxy_for {
my ($self, $host, $port) = @_;
my $p = $self->_proxy_uri_for($host, $port);
return defined($p) ? $p->uri_string : undef;
}
sub tcp_connect {
my ($self, $host, $port, $connect_cb, $prepare_cb) = @_;
my $proxy = $self->_proxy_uri_for($host, $port);
if(!defined($proxy)) {
return AnyEvent::Socket::tcp_connect $host, $port, $connect_cb, $prepare_cb;
}
return AnyEvent::Socket::tcp_connect $proxy->host, $proxy->port, sub {
my ($fh, $conn_host, $conn_port, $retry) = @_;
if(!defined($fh)) {
$connect_cb->();
return;
}
$proxy->establish_proxy($fh, $host, $port, sub {
my ($success) = @_;
$connect_cb->($success ? ($fh, $conn_host, $conn_port, $retry) : ());
});
}, $prepare_cb;
}
1;
__END__
=pod
=head1 NAME
AnyEvent::Connector - tcp_connect with transparent proxy handling
=head1 SYNOPSIS
use AnyEvent::Connector;
## Specify the proxy setting explicitly.
my $c = AnyEvent::Connector->new(
no_proxy => ['localhost', 'your-internal-domain.net']
);
## Proxy setting from "http_proxy" and "no_proxy" environment variables.
my $cenv = AnyEvent::Connector->new(
env_proxy => "http",
);
## Same API as AnyEvent::Socket::tcp_connect
my $guard = $c->tcp_connect(
"target.hogehoge.org", 80,
sub {
## connect callback
my ($fh ,$host, $port, $retry) = @_;
...;
},
sub {
## prepare calback
my ($fh) = @_;
...;
}
);
=head1 DESCRIPTION
L<AnyEvent::Connector> object has C<tcp_connect> method compatible
with that from L<AnyEvent::Socket>, and it handles proxy settings
transparently.
=head1 CLASS METHODS
=head2 $conn = AnyEvent::Connector->new(%args)
The constructor.
Fields in C<%args> are:
=over
=item C<proxy> => STR (optional)
String of proxy URL. Currently only C<http> proxy is supported.
If both C<proxy> and C<env_proxy> are not specified, the C<$conn> will directly connect to the destination host.
If both C<proxy> and C<env_proxy> are specified, setting by C<proxy> is used.
Setting empty string to C<proxy> disables the proxy setting done by C<env_proxy> option.
=item C<no_proxy> => STR or ARRAYREF of STR (optional)
String or array-ref of strings of domain names, to which the C<$conn> will directly connect.
If both C<no_proxy> and C<env_proxy> are specified, setting by C<no_proxy> is used.
Setting empty string or empty array-ref to C<no_proxy> disables the no_proxy setting done by C<env_proxy> option.
=item C<env_proxy> => STR (optional)
String of protocol specifier. If specified, proxy settings for that
protocol are loaded from environment variables, and C<$conn> is
created.
For example, if C<"http"> is specified, C<http_proxy> (or
C<HTTP_PROXY>) and C<no_proxy> (or C<NO_PROXY>) environment variables
are used to set C<proxy> and C<no_proxy> options, respectively.
C<proxy> and C<no_proxy> options have precedence over C<env_proxy>
option.
=back
=head1 OBJECT METHOD
=head2 $guard = $conn->tcp_connect($host, $port, $connect_cb, $prepare_cb)
Make a (possibly proxied) TCP connection to the given C<$host> and
C<$port>.
If C<< $conn->proxy_for($host, $port) >> returns C<undef>, the
behavior of this method is exactly the same as C<tcp_connect> function
from L<AnyEvent::Socket>.
If C<< $conn->proxy_for($host, $port) >> returns a proxy URL, it
behaves in the following way.
=over
=item *
It connects to the proxy, and tells the proxy to connect to the final
destination, C<$host> and C<$port>.
=item *
It runs C<$connect_cb> after the connection to the proxy AND
(hopefully) the connection between the proxy and the final destination
are both established.
$connect_cb->($cb_fh, $cb_host, $cb_port, $cb_retry)
C<$cb_fh> is the filehandle to the proxy. C<$cb_host> and C<$cb_port>
are the hostname and port of the proxy.
=item *
If the TCP connection to the proxy is established but the connection
to the final destination fails for some reason, C<$connect_cb> is
called with no argument passed (just as the original C<tcp_connect>
does).
=item *
If given, it runs C<$prepare_cb> before it starts connecting to the
proxy.
=back
=head2 $proxy = $conn->proxy_for($host, $port)
If C<$conn> uses a proxy to connect to the given C<$host> and
C<$port>, it returns the string of the proxy URL. Otherwise, it
returns C<undef>.
=head1 SEE ALSO
=over
=item *
L<AnyEvent::Socket>
=item *
L<AnyEvent::HTTP> - it has C<tcp_connect> option to implement proxy
connection. You can use L<AnyEvent::Connector> for it.
=back
=head1 REPOSITORY
=head1 BUGS AND FEATURE REQUESTS
Please report bugs and feature requests to my Github issues
Although I prefer Github, non-Github users can use CPAN RT
Please send email to C<bug-AnyEvent-Connector at rt.cpan.org> to report bugs
if you do not have CPAN RT account.
=head1 AUTHOR
Toshio Ito, C<< <toshioito at cpan.org> >>
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Toshio Ito.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See L<http://dev.perl.org/licenses/> for more information.
=cut