AnyEvent-SOCKS-Client
view release on metacpan or search on metacpan
lib/AnyEvent/SOCKS/Client.pm view on Meta::CPAN
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" )
: pack('CCnCCCCA*', 4, CMD_CONNECT, $port, 0,0,0,7 , "X\0$host\0" )
);
$self->{hd}->push_read( chunk => 8, sub{
my($code, $dst_port, $dst_ip) = unpack('xCna4', $_[1]);
unless( $code == 90 ){
AE::log "error" => "SOCKS4/4a request rejected: code is $code";
return;
}
$self->socks_connect_done( format_ipv4( $dst_ip ), $dst_port );
});
}
sub handshake{
my( $self ) = @_;
my $that = $self->{chain}->[0] ;
my @auth_methods = 0 ;
if($that->{login} and $that->{password}){
push @auth_methods, AUTH_LOGIN ;
}
$self->{hd}->push_write(
pack('CC', 5, scalar @auth_methods ) . join( "", map( pack( 'C', $_ ), @auth_methods ))
);
$self->{hd}->push_read( chunk => 2 , sub{
my $method = unpack( 'xC', $_[1] );
AE::log "debug" => "Server want auth method $method" ;
if($method == AUTH_GTFO ){
AE::log "error" => "Server: no suitable auth method";
return ;
}
if( $method ) {
$self->auth($method);
}
else {
$self->connect_cmd ;
}
});
}
sub auth{
my( $self, $method ) = @_;
my $that = $self->{chain}->[0] ;
if( $method == AUTH_LOGIN and $that->{login} and $that->{password}){
$self->{hd}->push_write(
pack('CC', 5, length $that->{login} ) . $that->{login}
. pack('C', length $that->{password}) . $that->{password}
);
$self->{hd}->push_read( chunk => 2, sub{
my $status = unpack('xC', $_[1]) ;
if( $status == 0 ){
$self->connect_cmd ;
return ;
}
AE::log "error" => "Bad login or password";
});
return ;
}
AE::log "error" => "Auth method $method not implemented!";
}
sub connect_cmd{
my( $self ) = @_ ;
my $next = $self->{chain}->[1] ;
my( $host, $port ) = $next
? ( $next->{host}, $next->{port} )
: ( $self->{dst_host}, $self->{dst_port} ) ;
my ($cmd, $ip );
if( $ip = parse_ipv4($host) ){
AE::log "debug" => "Connect IPv4: $host";
$cmd = pack('CCCCA4n', 5, CMD_CONNECT, 0, TYPE_IP4, $ip, $port);
}
elsif( $ip = parse_ipv6($host) ){
AE::log "debug" => "Connect IPv6: $host";
$cmd = pack('CCCCA16n', 5, CMD_CONNECT, 0, TYPE_IP6, $ip, $port);
}
else{
AE::log "debug" => "Connect hostname: $host";
$cmd = pack('CCCCCA*n', 5, CMD_CONNECT, 0, TYPE_FQDN , length $host, $host, $port);
}
$self->{hd}->push_write( $cmd );
$self->{hd}->push_read( chunk => 4, sub{
my( $status, $type ) = unpack( 'xCxC', $_[1] );
unless( $status == 0 ){
AE::log "error" => "Connect cmd rejected: status is $status" ;
return ;
}
$self->connect_cmd_finalize( $type );
});
}
sub connect_cmd_finalize{
my( $self, $type ) = @_ ;
AE::log "debug" => "Connect cmd done, bind atype is $type";
if($type == TYPE_IP4){
$self->{hd}->push_read( chunk => 6, sub{
my( $host, $port) = unpack( "a4n", $_[1] );
$self->socks_connect_done( format_ipv4( $host ), $port );
});
}
elsif($type == TYPE_IP6){
$self->{hd}->push_read( chunk => 18, sub{
my( $host, $port) = unpack( "a16n", $_[1] );
$self->socks_connect_done( format_ipv6( $host ) , $port );
});
}
elsif($type == TYPE_FQDN){
# read 1 byte (fqdn len)
# then read fqdn and port
$self->{hd}->push_read( chunk => 1, sub{
my $fqdn_len = unpack( 'C', $_[1] ) ;
( run in 2.470 seconds using v1.01-cache-2.11-cpan-cdf2f3d4e48 )