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 )