AnyEvent-SOCKS-Client
view release on metacpan or search on metacpan
lib/AnyEvent/SOCKS/Client.pm view on Meta::CPAN
=head1 VERSION
Version 0.051
=cut
=head1 SYNOPSIS
Constructs function which behave like AnyEvent::Socket::tcp_connect
use AnyEvent::SOCKS::Client qw/tcp_connect_via/;
$AnyEvent::SOCKS::Client::TIMEOUT = 30;
# used only if prepare_cb NOT passed to proxied function
# e.g. AE::HTTP on_prepare callback is not present
my @chain = qw/socks5://user:pass@192.0.2.100:1080 socks5://198.51.100.200:9080/;
tcp_connect_via( @chain )->( 'example.com', 80, sub{
my ($fh) = @_ or die "Connect failed $!";
...
});
SOCKS client for AnyEvent::HTTP
http_get "http://example.com/foo",
tcp_connect => tcp_connect_via('socks5://198.51.100.200:9080'),
sub{
my( $data, $header) = @_ ;
...
};
=head1 SECURITY
By default resolves names on SOCKS server. No DNS leaks.
=head1 SUBROUTINES/METHODS
=head2 $sub = tcp_connect_via( @proxy_uris )
Function accepts proxy list and return proxied tcp_connect function. See AnyEvent::Socket docs for more information about its semantics.
=cut
=head1 Errors and logging
Module uses AE::log for error reporting. You can use "error" or "debug" levels to get more information about errors.
=cut
package AnyEvent::SOCKS::Client;
use 5.006;
use strict ;
use AnyEvent;
use AnyEvent::Util qw/guard/;
use AnyEvent::Socket qw/tcp_connect parse_ipv4 format_ipv4 parse_ipv6 format_ipv6/;
use AnyEvent::Handle ;
use AnyEvent::Log ;
use Scalar::Util qw/weaken/;
require Exporter;
our $VERSION = '0.051';
our @ISA = qw/Exporter/;
our @EXPORT_OK = qw/tcp_connect_via/;
our $TIMEOUT = 300;
use constant {
TYPE_IP4 => 1,
TYPE_IP6 => 4,
TYPE_FQDN => 3,
AUTH_ANON => 0,
AUTH_GSSAPI => 1,
AUTH_LOGIN => 2,
AUTH_GTFO => 255,
CMD_CONNECT => 1 ,
CMD_BIND => 2,
CMD_UDP_ASSOC => 3,
};
sub _parse_uri{
my $re = qr!socks(4|4a|5)://(?:([^\s:]+):([^\s@]*)@)?(\[[0-9a-f:.]+\]|[^\s:]+):(\d+)!i ;
if( $_[0] =~ m/$re/gi ){
my $p = {v => $1, login => $2, password => $3, host => $4, port => $5};
$p->{host} =~ s/^\[|\]$//g;
return $p;
}
undef ;
}
# returns tcp_connect compatible function
sub tcp_connect_via{
my(@chain) = @_ ;
unless( @chain ){
AE::log "error" => "No socks were given, abort";
return sub{ $_[2]->() };
}
my @parsed;
for(@chain){
if( my $p = _parse_uri($_) ){
push @parsed, $p; next;
}
AE::log "error" => "Invalid socks uri: $_";
return sub{ $_[2]->() };
}
return sub{
my( $dst_host, $dst_port, $c_cb, $pre_cb ) = @_ ;
my $con = bless {
chain => \@parsed,
dst_host => $dst_host,
dst_port => $dst_port,
c_cb => $c_cb,
pre_cb => $pre_cb,
}, __PACKAGE__ ;
$con->connect;
if( defined wantarray ){ # not void
weaken( $con );
return guard{
AE::log "debug" => "Guard triggered" ;
if( ref $con eq __PACKAGE__ ){
undef $con->{c_cb};
$con->DESTROY;
}
};
}
undef;
};
}
sub connect{
my( $self ) = @_ ;
# tcp connect to first socks
my $that = $self->{chain}->[0] ;
$self->{_guard} = tcp_connect $that->{host}, $that->{port}, sub{
my $fh = shift ;
unless($fh){
AE::log "error" => "$that->{host}:$that->{port} connect failed: $!";
return;
}
$self->{hd} = new AnyEvent::Handle(
fh => $fh,
on_error => sub{
my ($hd, $fatal, $msg) = @_;
AE::log "error" => ( $fatal ? "Fatal " : "" ) . $msg ;
$hd->destroy unless( $hd->destroyed );
return;
}
);
if($that->{v} =~ /4a?/){
$self->connect_socks4;
return;
}
$self->handshake;
}, $self->{pre_cb} || sub{ $TIMEOUT };
}
sub connect_socks4{
my( $self ) = @_;
my( $that, $next ) = @{ $self->{chain} } ;
my( $host, $port ) = $next
? ( $next->{host}, $next->{port} )
: ( $self->{dst_host}, $self->{dst_port} ) ;
my $ip4 = parse_ipv4($host);
if( $that->{v} eq '4' and not $ip4 ){
AE::log "error" => "SOCKS4 is only support IPv4 addresses: $host given";
return;
}
if( $host =~ /:/ ){
AE::log "error" => "SOCKS4/4a doesn't support IPv6 addresses: $host given";
return;
}
AE::log "debug" => "SOCKS4 connect to $host:$port";
$self->{hd}->push_write( $ip4
? pack('CCnA4A2', 4, CMD_CONNECT, $port, $ip4, "X\0" )
( run in 3.760 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )