File Coverage

blib/lib/AnyEvent/SOCKS/Client.pm
Criterion Covered Total %
statement 26 171 15.2
branch 0 58 0.0
condition 0 20 0.0
subroutine 9 31 29.0
pod 1 8 12.5
total 36 288 12.5


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