File Coverage

blib/lib/AnyEvent/SOCKS/Client.pm
Criterion Covered Total %
statement 23 156 14.7
branch 0 56 0.0
condition 0 17 0.0
subroutine 8 28 28.5
pod 1 8 12.5
total 32 265 12.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             AnyEvent::SOCKS::Client - AnyEvent-based SOCKS client!
4              
5             =head1 VERSION
6              
7             Version 0.04
8              
9             =cut
10              
11             =head1 SYNOPSIS
12              
13             Constructs function which behave like AnyEvent::Socket::tcp_connect
14              
15             use AnyEvent::SOCKS::Client qw/tcp_connect_via/;
16              
17             $AnyEvent::SOCKS::Client::TIMEOUT = 30;
18             # used only if prepare_cb NOT passed to proxied function
19             # e.g. AE::HTTP on_prepare callback is not present
20              
21             my @chain = qw/socks5://user:pass@192.0.2.100:1080 socks5://198.51.100.200:9080/;
22             tcp_connect_via( @chain )->( 'example.com', 80, sub{
23             my ($fh) = @_ or die "Connect failed $!";
24             ...
25             });
26              
27             SOCKS client for AnyEvent::HTTP
28              
29             http_get "http://example.com/foo",
30             tcp_connect => tcp_connect_via('socks5://198.51.100.200:9080'),
31             sub{
32             my( $data, $header) = @_ ;
33             ...
34             };
35              
36             =head1 SECURITY
37              
38             By default resolves names on SOCKS server. No DNS leaks.
39              
40             =head1 SUBROUTINES/METHODS
41              
42             =head2 $sub = tcp_connect_via( @proxy_uris )
43              
44             Function accepts proxy list and return proxied tcp_connect function. See AnyEvent::Socket docs for more information about its semantics.
45              
46             =cut
47              
48             =head1 Errors and logging
49              
50             Module uses AE::log for error reporting. You can use "error" or "debug" levels to get more information about errors.
51              
52             =cut
53              
54             package AnyEvent::SOCKS::Client;
55              
56 1     1   68476 use 5.006;
  1         4  
57 1     1   6 use strict ;
  1         2  
  1         20  
58              
59 1     1   1062 use AnyEvent;
  1         5507  
  1         36  
60 1     1   583 use AnyEvent::Util qw/guard/;
  1         12855  
  1         73  
61 1     1   649 use AnyEvent::Socket qw/tcp_connect parse_ipv4 format_ipv4 parse_ipv6 format_ipv6/;
  1         15096  
  1         87  
62 1     1   786 use AnyEvent::Handle ;
  1         7934  
  1         40  
63 1     1   647 use AnyEvent::Log ;
  1         11473  
  1         100  
64              
65             require Exporter;
66             our $VERSION = '0.04';
67             our @ISA = qw/Exporter/;
68             our @EXPORT_OK = qw/tcp_connect_via/;
69              
70             our $TIMEOUT = 300;
71              
72             use constant {
73 1         2210 TYPE_IP4 => 1,
74             TYPE_IP6 => 4,
75             TYPE_FQDN => 3,
76            
77             AUTH_ANON => 0,
78             AUTH_GSSAPI => 1,
79             AUTH_LOGIN => 2,
80             AUTH_GTFO => 255,
81            
82             CMD_CONNECT => 1 ,
83             CMD_BIND => 2,
84             CMD_UDP_ASSOC => 3,
85 1     1   8 };
  1         2  
86              
87             sub _parse_uri{
88 0     0     my $re = qr!socks(4|4a|5)://(?:([^\s:]+):([^\s@]*)@)?(\[[0-9a-f:.]+\]|[^\s:]+):(\d+)!i ;
89 0 0         if( $_[0] =~ m/$re/gi ){
90 0           my $p = {v => $1, login => $2, password => $3, host => $4, port => $5};
91 0           $p->{host} =~ s/^\[|\]$//g;
92 0           return $p;
93             }
94 0           undef ;
95             }
96             # returns tcp_connect compatible function
97             sub tcp_connect_via{
98 0     0 1   my(@chain) = @_ ;
99             return sub{
100 0     0     my $con = bless { chain => [ map _parse_uri($_), @chain ] }, __PACKAGE__ ;
101 0           $con->connect( @_ ) ;
102 0           guard { undef };
  0            
103 0           };
104             }
105              
106             sub connect{
107 0     0 0   my( $self, $dst_host, $dst_port, $c_cb, $pre_cb ) = @_ ;
108 0 0         unless( @{ $self->{chain} } ){
  0            
109 0           AE::log "error" => "No socks were given, abort";
110 0           return $c_cb->();
111             }
112 0 0         if( $self->{c_cb}){
113 0           AE::log "error" => "It's one-off object, create another instance..";
114 0           return $c_cb->();
115             }
116 0           $self->{dst_host} = $dst_host;
117 0           $self->{dst_port} = $dst_port;
118 0           $self->{c_cb} = $c_cb ;
119              
120             # tcp connect to first socks
121 0           my $that = $self->{chain}->[0] ;
122             return tcp_connect $that->{host}, $that->{port}, sub{
123 0     0     my $fh = shift ;
124 0 0         unless($fh){
125 0           AE::log "error" => "$that->{host}:$that->{port} connect failed: $!";
126 0           return $c_cb->();
127             }
128             $self->{hd} = new AnyEvent::Handle(
129             fh => $fh,
130             on_error => sub{
131 0           my ($hd, $fatal, $msg) = @_;
132 0 0         AE::log "error" => ( $fatal ? "Fatal " : "" ) . $msg ;
133 0 0         $hd->destroy unless( $hd->destroyed );
134 0           $c_cb->();
135             }
136 0           );
137 0 0         if($that->{v} =~ /4a?/){
138 0           $self->connect_socks4;
139 0           return;
140             }
141 0           $self->handshake;
142 0   0 0     }, $pre_cb || sub{ $TIMEOUT };
  0            
143             }
144              
145             sub connect_socks4{
146 0     0 0   my( $self ) = @_;
147 0           my( $that, $next ) = @{ $self->{chain} } ;
  0            
148             my( $host, $port ) = $next
149             ? ( $next->{host}, $next->{port} )
150 0 0         : ( $self->{dst_host}, $self->{dst_port} ) ;
151              
152 0           my $ip4 = parse_ipv4($host);
153 0 0 0       if( $that->{v} eq '4' and not $ip4 ){
154 0           AE::log "error" => "SOCKS4 is only support IPv4 addresses: $host given";
155 0           return;
156             }
157              
158 0 0         if( $host =~ /:/ ){
159 0           AE::log "error" => "SOCKS4/4a doesn't support IPv6 addresses: $host given";
160 0           return;
161             }
162              
163 0 0         $self->{hd}->push_write( $ip4
164             ? pack('CCnA4A2', 4, CMD_CONNECT, $port, $ip4, "X\0" )
165             : pack('CCnCCCCA*', 4, CMD_CONNECT, $port, 0,0,0,7 , "X\0$host\0" )
166             );
167             $self->{hd}->push_read( chunk => 8, sub{
168 0     0     my($code, $dst_port, $dst_ip) = unpack('xCna4', $_[1]);
169 0 0         unless( $code == 90 ){
170 0           AE::log "error" => "SOCKS4/4a request rejected: code is $code";
171 0           return;
172             }
173 0           $self->socks_connect_done( format_ipv4( $dst_ip ), $dst_port );
174 0           });
175             }
176              
177             sub handshake{
178 0     0 0   my( $self ) = @_;
179 0           my $that = $self->{chain}->[0] ;
180 0           my @auth_methods = 0 ;
181 0 0 0       if($that->{login} and $that->{password}){
182 0           push @auth_methods, AUTH_LOGIN ;
183             }
184             $self->{hd}->push_write(
185 0           pack('CC', 5, scalar @auth_methods ) . join( "", map( pack( 'C', $_ ), @auth_methods ))
186             );
187             $self->{hd}->push_read( chunk => 2 , sub{
188 0     0     my $method = unpack( 'xC', $_[1] );
189 0           AE::log "debug" => "Server want auth method $method" ;
190 0 0         if($method == AUTH_GTFO ){
191 0           AE::log "error" => "Server: no suitable auth method";
192 0           return ;
193             }
194              
195 0 0         if( $method ) {
196 0           $self->auth($method);
197             }
198             else {
199 0           $self->connect_cmd ;
200             }
201 0           });
202             }
203              
204             sub auth{
205 0     0 0   my( $self, $method ) = @_;
206 0           my $that = $self->{chain}->[0] ;
207 0 0 0       if( $method == AUTH_LOGIN and $that->{login} and $that->{password}){
      0        
208             $self->{hd}->push_write(
209             pack('CC', 5, length $that->{login} ) . $that->{login}
210             . pack('C', length $that->{password}) . $that->{password}
211 0           );
212             $self->{hd}->push_read( chunk => 2, sub{
213 0     0     my $status = unpack('xC', $_[1]) ;
214 0 0         if( $status == 0 ){
215 0           $self->connect_cmd ;
216 0           return ;
217             }
218 0           AE::log "error" => "Bad login or password";
219 0           });
220 0           return ;
221             }
222 0           AE::log "error" => "Auth method $method not implemented!";
223             }
224              
225             sub connect_cmd{
226 0     0 0   my( $self ) = @_ ;
227 0           my $next = $self->{chain}->[1] ;
228             my( $host, $port ) = $next
229             ? ( $next->{host}, $next->{port} )
230 0 0         : ( $self->{dst_host}, $self->{dst_port} ) ;
231              
232 0           my ($cmd, $ip );
233 0 0         if( $ip = parse_ipv4($host) ){
    0          
234 0           AE::log "debug" => "Connect IPv4: $host";
235 0           $cmd = pack('CCCCA4n', 5, CMD_CONNECT, 0, TYPE_IP4, $ip, $port);
236             }
237             elsif( $ip = parse_ipv6($host) ){
238 0           AE::log "debug" => "Connect IPv6: $host";
239 0           $cmd = pack('CCCCA16n', 5, CMD_CONNECT, 0, TYPE_IP6, $ip, $port);
240             }
241             else{
242 0           AE::log "debug" => "Connect hostname: $host";
243 0           $cmd = pack('CCCCCA*n', 5, CMD_CONNECT, 0, TYPE_FQDN , length $host, $host, $port);
244             }
245              
246 0           $self->{hd}->push_write( $cmd );
247             $self->{hd}->push_read( chunk => 4, sub{
248 0     0     my( $status, $type ) = unpack( 'xCxC', $_[1] );
249 0 0         unless( $status == 0 ){
250 0           AE::log "error" => "Connect cmd rejected: status is $status" ;
251 0           return ;
252             }
253 0           $self->connect_cmd_finalize( $type );
254 0           });
255             }
256              
257             sub connect_cmd_finalize{
258 0     0 0   my( $self, $type ) = @_ ;
259              
260 0           AE::log "debug" => "Connect cmd done, bind atype is $type";
261              
262 0 0         if($type == TYPE_IP4){
    0          
    0          
263             $self->{hd}->push_read( chunk => 6, sub{
264 0     0     my( $host, $port) = unpack( "a4n", $_[1] );
265 0           $self->socks_connect_done( format_ipv4( $host ), $port );
266 0           });
267             }
268             elsif($type == TYPE_IP6){
269             $self->{hd}->push_read( chunk => 18, sub{
270 0     0     my( $host, $port) = unpack( "a16n", $_[1] );
271 0           $self->socks_connect_done( format_ipv6( $host ) , $port );
272 0           });
273             }
274             elsif($type == TYPE_FQDN){
275             # read 1 byte (fqdn len)
276             # then read fqdn and port
277             $self->{hd}->push_read( chunk => 1, sub{
278 0     0     my $fqdn_len = unpack( 'C', $_[1] ) ;
279             $self->{hd}->push_read( chunk => $fqdn_len + 2 , sub{
280 0           my $host = substr( $_[1], 0, $fqdn_len ) ;
281 0           my $port = unpack('n', substr( $_[1], -2) );
282 0           $self->socks_connect_done( $host, $port );
283 0           });
284 0           });
285             }
286             else{
287 0           AE::log "error" => "Unknown atype $type";
288             }
289             }
290              
291             sub socks_connect_done{
292 0     0 0   my( $self, $bind_host, $bind_port ) = @_;
293              
294 0           my $that = shift @{ $self->{chain} }; # shift = move forward in chain
  0            
295 0           AE::log "debug" => "Done with server socks$that->{v}://$that->{host}:$that->{port} , bound to $bind_host:$bind_port";
296              
297 0 0         if( @{ $self->{chain} } ){
  0            
298 0           $self->handshake ;
299 0           return ;
300             }
301              
302 0           AE::log "debug" => "Giving up fh and returning to void...";
303 0 0         $self->{c_cb}->( $self->{hd}->fh ) and undef $self->{c_cb};
304             }
305              
306             sub DESTROY {
307 0     0     my $self = shift ;
308 0           AE::log "debug" => "Kitten saver called";
309 0 0 0       $self->{hd}->destroy if( $self->{hd} and not $self->{hd}->destroyed );
310 0 0         $self->{c_cb}->() if( $self->{c_cb} );
311 0           undef %$self;
312             }
313              
314              
315             =head1 AUTHOR
316              
317             Zlobus, C<< >>
318              
319             =head1 BUGS
320              
321             Please report any bugs or feature requests to C, or through
322             the web interface at L. I will be notified, and then you'll
323             automatically be notified of progress on your bug as I make changes.
324              
325              
326              
327              
328             =head1 SUPPORT
329              
330             You can find documentation for this module with the perldoc command.
331              
332             perldoc AnyEvent::SOCKS::Client
333              
334              
335             You can also look for information at:
336              
337             =over 4
338              
339             =item * RT: CPAN's request tracker (report bugs here)
340              
341             L
342              
343             =item * AnnoCPAN: Annotated CPAN documentation
344              
345             L
346              
347             =item * CPAN Ratings
348              
349             L
350              
351             =item * Search CPAN
352              
353             L
354              
355             =back
356              
357              
358             =head1 ACKNOWLEDGEMENTS
359              
360             URI parser copied from AnyEvent::HTTP::Socks
361              
362              
363             =head1 LICENSE AND COPYRIGHT
364              
365             Copyright 2018 Zlobus.
366              
367             This program is free software; you can redistribute it and/or modify it
368             under the terms of the the Artistic License (2.0). You may obtain a
369             copy of the full license at:
370              
371             L
372              
373             Any use, modification, and distribution of the Standard or Modified
374             Versions is governed by this Artistic License. By using, modifying or
375             distributing the Package, you accept this license. Do not use, modify,
376             or distribute the Package, if you do not accept this license.
377              
378             If your Modified Version has been derived from a Modified Version made
379             by someone other than you, you are nevertheless required to ensure that
380             your Modified Version complies with the requirements of this license.
381              
382             This license does not grant you the right to use any trademark, service
383             mark, tradename, or logo of the Copyright Holder.
384              
385             This license includes the non-exclusive, worldwide, free-of-charge
386             patent license to make, have made, use, offer to sell, sell, import and
387             otherwise transfer the Package with respect to any patent claims
388             licensable by the Copyright Holder that are necessarily infringed by the
389             Package. If you institute patent litigation (including a cross-claim or
390             counterclaim) against any party alleging that the Package constitutes
391             direct or contributory patent infringement, then this Artistic License
392             to you shall terminate on the date that such litigation is filed.
393              
394             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
395             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
396             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
397             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
398             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
399             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
400             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
401             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
402              
403              
404             =cut
405              
406             1; # End of AnyEvent::SOCKS::Client