File Coverage

blib/lib/POE/Component/Client/FTP.pm
Criterion Covered Total %
statement 263 428 61.4
branch 67 120 55.8
condition 24 56 42.8
subroutine 37 59 62.7
pod 1 51 1.9
total 392 714 54.9


line stmt bran cond sub pod time code
1             # POE::Component::Client::FTP
2             #
3             # Author : Michael Ching
4             # Email : michaelc@wush.net
5             # Created : May 15, 2002
6             # Description : An FTP client for POE
7              
8             package POE::Component::Client::FTP;
9             $POE::Component::Client::FTP::VERSION = '0.24';
10             #ABSTRACT: Implements an FTP client POE Component
11              
12 4     4   341351 use strict;
  4         5  
  4         91  
13 4     4   13 use warnings;
  4         4  
  4         100  
14              
15 4     4   13 use Carp;
  4         4  
  4         160  
16 4     4   13 use Exporter;
  4         3  
  4         114  
17 4     4   15 use Socket;
  4         4  
  4         1770  
18             # use Data::Dumper;
19              
20 4         36 use POE qw(Wheel::SocketFactory Wheel::ReadWrite
21 4     4   16 Filter::Stream Filter::Line Driver::SysRW);
  4         6  
22              
23 4     4   53282 use vars qw(@ISA @EXPORT $poe_kernel);
  4         5  
  4         284  
24              
25             @ISA = qw(Exporter);
26             @EXPORT = qw(FTP_PASSIVE FTP_ACTIVE FTP_MANUAL FTP_ASCII FTP_BINARY);
27              
28             BEGIN {
29 4 50   4   137 eval 'sub DEBUG () { 0 }' unless defined &DEBUG;
30 4 50       92 eval 'sub DEBUG_COMMAND () { 0 }' unless defined &DEBUG_COMMAND;
31 4 50       16604 eval 'sub DEBUG_DATA () { 0 }' unless defined &DEBUG_DATA;
32             }
33              
34             sub EOL () { "\015\012" }
35              
36             # connection modes
37             sub FTP_PASSIVE () { 1 }
38             sub FTP_ACTIVE () { 2 }
39              
40             # transfer modes
41             sub FTP_MANUAL () { 0 }
42             sub FTP_ASCII () { 1 }
43             sub FTP_BINARY () { 2 }
44              
45             # tells the dispatcher which states support which events
46             my $state_map =
47             { _init => { "_start" => \&do_init_start,
48             "connect" => \&do_init_start,
49             "cmd_connected" => \&handler_init_connected,
50             "cmd_connect_error" => \&handler_init_error,
51             "success" => \&handler_init_success,
52             "timeout" => \&handler_init_error
53             },
54              
55             stop => { "_start" => \&do_stop
56             },
57              
58             authtls => { "authtls" => \&do_send_authtls,
59             "success" => \&handler_authtls_success,
60             "failure" => \&handler_authtls_failure
61             },
62              
63             login => { "login" => \&do_login_send_username,
64             "intermediate" => \&do_login_send_password,
65             "success" => \&handler_login_success,
66             "failure" => \&handler_login_failure
67             },
68              
69             pbsz_prot => { "pbsz" => \&do_send_pbsz,
70             "prot" => \&do_send_prot,
71             "success" => \&handler_pbsz_prot_success,
72             "failure" => \&handler_pbsz_prot_failure
73             },
74              
75             global => { "cmd_input" => \&handler_cmd_input,
76             "cmd_error" => \&handler_cmd_error
77             },
78              
79             ready => { "_start" => \&dequeue_event,
80             rename => \&do_rename,
81             put => \&do_put,
82              
83             map ( { $_ => \&do_simple_command }
84             qw{ cd cdup delete mdtm mkdir noop
85             pwd rmdir site size stat syst type quit quot }
86             ),
87              
88             map ( { $_ => \&do_complex_command }
89             qw{ dir ls get }
90             ),
91              
92             map ( { $_ => \&do_set_attribute }
93             qw{ trans_mode conn_mode blocksize timeout }
94             ),
95             },
96              
97             put => { data_flush => \&handler_put_flushed,
98             data_error => \&handler_put_data_error,
99              
100             put_data => \&do_put_data,
101             put_close => \&do_put_close,
102              
103             data_connected => \&handler_complex_connected,
104             data_connect_error => \&handler_complex_connect_error,
105             preliminary => \&handler_complex_preliminary,
106             success => \&handler_complex_success,
107             failure => \&handler_complex_failure,
108             },
109              
110             rename => { "intermediate" => \&handler_rename_intermediate,
111             "success" => \&handler_rename_success,
112             "failure" => \&handler_rename_failure
113             },
114              
115             default_simple => { "success" => \&handler_simple_success,
116             "failure" => \&handler_simple_failure
117             },
118              
119             default_complex => { data_connected => \&handler_complex_connected,
120             data_connect_error => \&handler_complex_connect_error,
121             data_flush => \&handler_complex_flushed,
122             preliminary => \&handler_complex_preliminary,
123             success => \&handler_complex_success,
124             failure => \&handler_complex_failure,
125             data_input => \&handler_complex_list_data,
126             data_error => \&handler_complex_list_error
127             },
128             };
129              
130             # translation from posted signals to ftp commands
131             my %command_map = ( CD => "CWD",
132             MKDIR => "MKD",
133             RMDIR => "RMD",
134              
135             LS => "LIST",
136             DIR => "NLST",
137             GET => "RETR",
138              
139             PUT => "STOR",
140              
141             DELETE => "DELE",
142             );
143              
144             # create a new POE::Component::Client::FTP object
145             sub spawn {
146 4     4 1 7645 my $class = shift;
147 4         18 my $sender = $poe_kernel->get_active_session;
148              
149 4 50       23 croak "$class->spawn requires an event number of argument" if @_ & 1;
150              
151 4         24 my %params = @_;
152              
153 4         10 my $alias = delete $params{Alias};
154 4 50       12 croak "$class->spawn requires an alias to start" unless defined $alias;
155              
156 4         8 my $user = delete $params{Username};
157 4         9 my $pass = delete $params{Password};
158              
159 4         8 my $local_addr = delete $params{LocalAddr};
160 4 100       19 $local_addr = INADDR_ANY unless defined $local_addr;
161              
162 4         7 my $local_port = delete $params{LocalPort};
163 4 50       11 $local_port = 0 unless defined $local_port;
164              
165 4         6 my $remote_addr = delete $params{RemoteAddr};
166 4 50       10 croak "$class->spawn requires a RemoteAddr parameter"
167             unless defined $remote_addr;
168              
169 4         6 my $remote_port = delete $params{RemotePort};
170 4 50       10 $remote_port = 21 unless defined $remote_port;
171              
172 4         5 my $tlscmd = delete $params{TLS};
173 4 50       14 $tlscmd = 0 unless defined $tlscmd;
174              
175 4         4 my $tlsdata = delete $params{TLSData};
176 4 50       13 $tlsdata = 0 unless defined $tlsdata;
177              
178 4         6 my $timeout = delete $params{Timeout};
179 4 50       11 $timeout = 120 unless defined $timeout;
180              
181 4         6 my $blocksize = delete $params{BlockSize};
182 4 50       11 $blocksize = 10240 unless defined $blocksize;
183              
184 4         11 my $conn_mode = delete $params{ConnectionMode};
185 4 100       62 $conn_mode = FTP_PASSIVE unless defined $conn_mode;
186              
187 4         7 my $trans_mode = delete $params{TransferMode};
188 4 50       10 $trans_mode = FTP_MANUAL unless defined $trans_mode;
189              
190 4         6 my $filters = delete $params{Filters};
191 4   33     29 $filters->{dir} ||= new POE::Filter::Line( Literal => EOL );
192 4   33     146 $filters->{ls} ||= new POE::Filter::Line( Literal => EOL );
193 4   33     83 $filters->{get} ||= new POE::Filter::Stream();
194 4   33     47 $filters->{put} ||= new POE::Filter::Stream();
195              
196 4         35 my $events = delete $params{Events};
197 4 50 33     29 $events = [] unless defined $events and ref( $events ) eq 'ARRAY';
198 4         5 my %register;
199 4         8 for my $opt ( @$events ) {
200 41 50       38 if ( ref $opt eq 'HASH' ) {
201 0         0 @register{keys %$opt} = values %$opt;
202             } else {
203 41         62 $register{$opt} = $opt;
204             }
205             }
206              
207             # Make sure the user didn't make a typo on parameters
208 4 50       17 carp "Unknown parameters: ", join( ', ', sort keys %params )
209             if keys %params;
210              
211 4 50 33     21 if ( $tlscmd || $tlsdata ) {
212 0         0 eval {
213 0         0 require POE::Component::SSLify;
214 0         0 import POE::Component::SSLify qw( Client_SSLify );
215             };
216              
217 0 0       0 if ($@) {
218 0         0 warn "TLS option specified, but there was a problem\n";
219 0         0 $tlscmd = 0; $tlsdata = 0;
  0         0  
220             }
221             }
222              
223 4         90 my $self = bless {
224             alias => $alias,
225             user => $user,
226             pass => $pass,
227             local_addr => $local_addr,
228             local_port => $local_port,
229             remote_addr => $remote_addr,
230             remote_port => $remote_port,
231              
232             tlscmd => $tlscmd,
233             tlsdata => $tlsdata,
234             attr_trans_mode => $trans_mode,
235             attr_conn_mode => $conn_mode,
236             attr_timeout => $timeout,
237             attr_blocksize => $blocksize,
238              
239             cmd_sock_wheel => undef,
240             cmd_rw_wheel => undef,
241              
242             data_sock_wheel => undef,
243             data_rw_wheel => undef,
244             data_sock_port => 0,
245             data_suicidal => 0,
246              
247             filters => $filters,
248              
249             state => "_init",
250             queue => [ ],
251              
252             stack => [ [ 'init' ] ],
253             event => [ ],
254              
255             complex_stack => [ ],
256              
257             events => { $sender => \%register }
258             }, $class;
259              
260 4         25 $self->{session_id} = POE::Session->create (
261             inline_states => map_all($state_map, \&dispatch),
262              
263             heap => $self,
264             )->ID();
265 4         399 return $self;
266             }
267              
268             # connect to address specified during spawn
269             sub do_init_start {
270 4     4 0 12 my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP];
271 4         11 $heap->{session_id} = $session->ID();
272             # set timeout for connection
273 4         33 $kernel->delay( "timeout", $heap->{attr_timeout}, undef, undef, "Timeout ($heap->{attr_timeout} seconds)" );
274              
275             # connect to command port
276             $heap->{cmd_sock_wheel} = POE::Wheel::SocketFactory->new(
277             SocketDomain => AF_INET,
278             SocketType => SOCK_STREAM,
279             SocketProtocol => 'tcp',
280             RemotePort => $heap->{remote_port},
281             RemoteAddress => $heap->{remote_addr},
282 4         350 SuccessEvent => 'cmd_connected',
283             FailureEvent => 'cmd_connect_error'
284             );
285              
286 4         1545 $kernel->alias_set( $heap->{alias} );
287 4         113 return;
288             }
289              
290             # try to clean up
291             # client responsibility to ensure things are all complete
292             sub do_stop {
293 4     4 0 14 my $heap = $poe_kernel->get_active_session()->get_heap();
294              
295 4         16 warn "cleaning up" if DEBUG;
296              
297 4         12 delete $heap->{cmd_rw_wheel};
298 4         488 delete $heap->{cmd_sock_wheel};
299 4         52 clean_up_complex_cmd();
300              
301 4         17 $poe_kernel->alias_remove( $heap->{alias} );
302 4         128 return;
303             }
304              
305             # server responses on command connection
306             sub handler_cmd_input {
307 47     47 0 70 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
308              
309 47         27 warn "<<< $input\n" if DEBUG_COMMAND;
310              
311 47         40 my $coderef;
312              
313 47         80 my ($code, $more) = parse_cmd_string(\$input);
314              
315 47 100 66     185 $input =~ s/^ // if defined $more && $more eq "-";
316              
317 47         93 $heap->{ftp_message} .= "$input\n";
318              
319 47 100 33     235 return unless defined $code && defined $more && $more eq " ";
      66        
320              
321 31         141 $heap->{ftp_message} =~ s/\s+$//;
322              
323 31         56 my $major = substr($code, 0, 1);
324              
325 31 100       99 if ($major == 1) {
    100          
    50          
326             # 1yz Positive Preliminary reply
327              
328 3         9 $coderef = $state_map->{ $heap->{state} }{preliminary};
329             }
330             elsif ($major == 2) {
331             # 2yz Positive Completion reply
332              
333 24         45 $coderef = $state_map->{ $heap->{state} }{success};
334 24   50     16 $heap->{event} = pop( @{$heap->{stack}} ) || ['none', {}];
335             }
336             elsif ($major == 3) {
337             # 3yz Positive Intermediate reply
338              
339 4         10 $coderef = $state_map->{ $heap->{state} }{intermediate};
340 4   50     7 $heap->{event} = pop( @{$heap->{stack}} ) || ['none', {}];
341             }
342             else {
343             # 4yz Transient Negative Completion reply
344             # 5yz Permanent Negative Completion reply
345              
346 0         0 $coderef = $state_map->{ $heap->{state} }{failure};
347 0   0     0 $heap->{event} = pop( @{$heap->{stack}} ) || ['none', {}];
348             }
349              
350 31 50       73 &{ $coderef }(@_) if $coderef;
  31         66  
351 31         47 delete $heap->{ftp_message};
352 31         37 return;
353             }
354              
355              
356             # command connection closed
357             sub handler_cmd_error {
358 4     4 0 12 my ($kernel, $heap) = @_[KERNEL, HEAP];
359              
360 4         11 goto_state("stop");
361 4         4 return;
362             }
363              
364             ## state specific
365              
366             ## rename state
367              
368             # initiate multipart rename command
369             # uses the complex_stack to remember what to do next
370             sub do_rename {
371 0     0 0 0 my ($kernel, $heap, $event, $fr, $to) = @_[KERNEL, HEAP, STATE, ARG0, ARG1];
372              
373 0         0 goto_state("rename");
374              
375 0         0 $heap->{complex_stack} = [ "RNTO", $to ];
376 0         0 command( [ "RNFR", $fr ] );
377 0         0 return;
378             }
379              
380             # successful RNFR
381             sub handler_rename_intermediate {
382 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
383              
384 0         0 my $status = substr($input, 0, 3);
385 0         0 my $line = substr($input, 4);
386              
387             send_event( "rename_partial",
388             $status, $line,
389 0         0 $heap->{event}->[1] );
390              
391 0         0 command( $heap->{complex_stack} );
392 0         0 return;
393             }
394              
395             # successful RNTO
396             sub handler_rename_success {
397 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
398              
399 0         0 my $status = substr($input, 0, 3);
400 0         0 my $line = substr($input, 4);
401              
402             send_event( "rename",
403             $status, $line,
404 0         0 $heap->{event}->[1] );
405              
406 0         0 delete $heap->{complex_stack};
407 0         0 goto_state("ready");
408 0         0 return;
409             }
410              
411             # failed RNFR or RNTO
412             sub handler_rename_failure {
413 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
414              
415 0         0 my $status = substr($input, 0, 3);
416 0         0 my $line = substr($input, 4);
417              
418             send_event( "rename_error",
419             $status, $line,
420 0         0 $heap->{event}->[1] );
421              
422 0         0 delete $heap->{complex_stack};
423 0         0 goto_state("ready");
424 0         0 return;
425             }
426              
427             # initiate a STOR command
428             sub do_put {
429 0     0 0 0 my ($kernel, $heap, $event) = @_[KERNEL, HEAP, STATE];
430              
431 0         0 goto_state("put");
432              
433 0         0 $heap->{complex_stack} = { command => [$event, @_[ARG0..$#_]],
434             sendq => [],
435             };
436              
437 0         0 establish_data_conn();
438 0         0 return;
439             }
440              
441             # socket flushed, see if socket can be closed
442             sub handler_put_flushed {
443 0     0 0 0 my ($kernel, $heap, $session) = @_[KERNEL, HEAP];
444              
445 0         0 warn "data flushed " . $heap->{complex_stack}->{last_length} if DEBUG;
446              
447             # if no packet was pending, this was simply to get things going or to
448             # check for suicide and thus will confuse the user if sent
449 0 0       0 if ($heap->{complex_stack}->{pending}) {
450 0         0 $heap->{complex_stack}->{pending} = 0;
451              
452             send_event( "put_flushed",
453             $heap->{complex_stack}->{last_length},
454             $heap->{complex_stack}->{command}->[1] )
455 0 0       0 if $heap->{complex_stack}->{last_length};
456             }
457              
458 0         0 warn "Q||: " . scalar @{$heap->{complex_stack}->{sendq}} if DEBUG;
459              
460             # we use an internal sendq and send lines as each line is sent
461             # this way we can give the user feedback as to the status of the upload
462             # so, whenever data is flushed send the next packet
463 0 0       0 if ( defined(my $line = shift @{$heap->{complex_stack}->{sendq}}) ) {
  0 0       0  
464 0         0 warn "sending queued packet: " . length ($line) if DEBUG;
465              
466 0         0 $heap->{complex_stack}->{pending} = 1;
467 0         0 $heap->{data_rw_wheel}->put($line);
468 0         0 $heap->{complex_stack}->{last_length} = length $line;
469             }
470             elsif ($heap->{data_suicidal}) {
471 0         0 warn "killing suicidal socket" . $heap->{data_rw_wheel}->get_driver_out_octets() if DEBUG;
472              
473 0         0 clean_up_complex_cmd();
474 0         0 $heap->{data_suicidal} = 0;
475              
476             send_event("put_closed",
477 0         0 $heap->{complex_stack}->{command}->[1]);
478 0         0 goto_state("ready");
479             }
480 0         0 return;
481             }
482              
483             # remote end closed data connection
484             # in put this in an error, not a normal condition
485             sub handler_put_data_error {
486 0     0 0 0 my ($kernel, $heap, $error) = @_[KERNEL, HEAP, ARG0];
487              
488             send_event( "put_error", $error,
489 0         0 $heap->{complex_stack}->{command}->[1] );
490              
491 0         0 clean_up_complex_cmd();
492 0         0 goto_state("ready");
493 0         0 return;
494             }
495              
496             # client sending data for us to print to the STOR
497             sub do_put_data {
498 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
499 0         0 warn "put: " . length($input) if DEBUG;
500              
501             # add to send queue
502              
503 0         0 $kernel->call('cmd_connected', @{$heap->{cmd_sock_wheel}}[0]);
  0         0  
504 0         0 push @{ $heap->{complex_stack}->{sendq} }, $input;
  0         0  
505              
506             # send the first flushed event if this was the first item
507 0 0 0     0 unless ( @{ $heap->{complex_stack}->{sendq} } > 1
  0         0  
508             or $heap->{complex_stack}->{pending} ) {
509 0         0 $kernel->yield('data_flush');
510             }
511 0         0 return;
512             }
513              
514             # client request to end STOR command
515             sub do_put_close {
516 0     0 0 0 my ($kernel, $heap) = @_[KERNEL, HEAP];
517 0         0 warn "setting suicidal on" if DEBUG;
518              
519 0         0 $heap->{data_suicidal} = 1;
520              
521             # if close is called when sendq is empty, we'll need to fake a flush
522 0 0 0     0 unless ( @{ $heap->{complex_stack}->{sendq} } > 0
  0         0  
523             or $heap->{complex_stack}->{pending} ) {
524 0         0 warn "empty sendq, manually flushing" if DEBUG;
525 0         0 $kernel->yield('data_flush');
526             }
527 0         0 return;
528             }
529              
530             ## login state
531              
532             # connection established, create a rw wheel
533             sub handler_init_connected {
534 4     4 0 12 my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];
535              
536             # clear the timeout
537 4         18 $kernel->delay("timeout");
538              
539 4         235 $heap->{cmd_rw_wheel} = POE::Wheel::ReadWrite->new(
540             Handle => $socket,
541             Filter => POE::Filter::Line->new( Literal => EOL ),
542             Driver => POE::Driver::SysRW->new(),
543             InputEvent => 'cmd_input',
544             ErrorEvent => 'cmd_error'
545             );
546 4         743 return;
547             }
548              
549             # connect to server failed, clean up
550             sub handler_init_error {
551 0     0 0 0 my ($kernel, $heap, $errnum, $errstr) = @_[KERNEL, HEAP, ARG1, ARG2];
552              
553             # clear the timeout
554 0         0 $kernel->delay("timeout");
555              
556 0         0 delete $heap->{cmd_sock_wheel};
557 0         0 send_event( "connect_error", $errnum, $errstr );
558 0         0 return;
559             }
560              
561             # wheel established, log in if we can
562             sub handler_init_success {
563 4     4 0 10 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
564              
565 4         9 my $status = substr($input, 0, 3);
566 4         7 my $line = substr($input, 4);
567              
568 4         12 send_event( "connected",
569             $status, $line );
570              
571              
572 4 50       9 if ($heap->{tlscmd}) {
573 0         0 goto_state("authtls");
574 0         0 $kernel->yield("authtls");
575             }
576             else {
577 4         12 goto_state("login");
578              
579 4 50 33     25 if ( defined $heap->{user} and defined $heap->{pass} ) {
580 4         13 $kernel->yield("login");
581             }
582             }
583 4         149 return;
584             }
585              
586             # start the tls negotiation on the control connection by sending "AUTH TLS"
587             sub do_send_authtls {
588 0     0 0 0 my ($kernel, $heap) = @_[KERNEL, HEAP];
589 0         0 command( [ 'AUTH', 'TLS'] );
590 0         0 return;
591             }
592              
593             sub handler_authtls_success {
594 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
595              
596 0         0 my $socket = $heap->{cmd_rw_wheel}->get_input_handle();
597 0         0 delete $heap->{cmd_rw_wheel};
598              
599 0         0 eval { $socket = Client_SSLify( $socket, 'tlsv1' )};
  0         0  
600 0 0       0 if ( $@ ) {
601 0         0 die "Unable to SSLify control connection: $@";
602             }
603              
604             # set up the rw wheel again
605              
606 0         0 $heap->{cmd_rw_wheel} = POE::Wheel::ReadWrite->new(
607             Handle => $socket,
608             Filter => POE::Filter::Line->new( Literal => EOL ),
609             Driver => POE::Driver::SysRW->new(),
610             InputEvent => 'cmd_input',
611             ErrorEvent => 'cmd_error'
612             );
613              
614 0         0 goto_state("login");
615 0         0 $kernel->yield("login");
616 0         0 return;
617             }
618              
619             sub handler_authtls_failure {
620 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
621              
622 0         0 my $status = substr($input, 0, 3);
623 0         0 my $line = substr($input, 4);
624              
625 0         0 send_event( "login_error",
626             $status, $line );
627 0         0 return;
628             }
629              
630             # login using parameters specified during spawn or passed in now
631             sub do_login_send_username {
632 4     4 0 11 my ($kernel, $heap, $user, $pass) = @_[KERNEL, HEAP, ARG0 .. ARG1];
633              
634 4 50       19 $heap->{user} = $user unless defined $heap->{user};
635 4 50       12 croak "No username defined in login" unless defined $heap->{user};
636 4 50       13 $heap->{pass} = $pass unless defined $heap->{pass};
637 4 50       15 croak "No password defined in login" unless defined $heap->{pass};
638              
639 4         20 command( [ 'USER', $heap->{user} ] );
640 4         178 delete $heap->{user};
641 4         4 return;
642             }
643              
644             # username accepted
645             sub do_login_send_password {
646 4     4 0 8 my ($kernel, $heap) = @_[KERNEL, HEAP];
647              
648 4         14 command( [ 'PASS', $heap->{pass} ] );
649 4         165 delete $heap->{pass};
650 4         6 return;
651             }
652              
653             sub handler_login_success {
654 4     4 0 14 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
655              
656 4         9 my $status = substr($input, 0, 3);
657 4         10 my $line = substr($input, 4);
658              
659 4 50       14 if ($heap->{tlsdata}) {
660 0         0 goto_state("pbsz_prot");
661 0         0 $kernel->yield('pbsz');
662             }
663             else {
664 4         10 send_event( "authenticated",
665             $status, $line );
666              
667 4         9 goto_state("ready");
668             }
669 4         95 return;
670             }
671              
672             sub handler_login_failure {
673 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
674              
675 0         0 my $status = substr($input, 0, 3);
676 0         0 my $line = substr($input, 4);
677              
678 0         0 send_event( "login_error",
679             $status, $line );
680 0         0 return;
681             }
682              
683              
684             # PBSZ 0 and PROT P are needed to encrypt the data connection (specified with TLSData)
685             # this is done _before_ 'authenticated' is sent to the user session (even though the u/p is already accepted)
686              
687             sub do_send_pbsz {#
688 0     0 0 0 my ($kernel, $heap) = @_[KERNEL, HEAP];
689 0         0 command( [ 'PBSZ', '0' ] );
690 0         0 return;
691             }
692              
693             sub do_send_prot {#
694 0     0 0 0 my ($kernel, $heap) = @_[KERNEL, HEAP];
695 0         0 command( [ 'PROT', 'P' ] );
696 0         0 return;
697             }
698              
699              
700             sub handler_pbsz_prot_success {
701 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
702              
703 0 0       0 if ($heap->{event}->[0] eq "PBSZ") {
704 0         0 $kernel->yield("prot");
705             }
706             else {
707 0         0 my $status = substr($input, 0, 3);
708 0         0 my $line = substr($input, 4);
709              
710 0         0 send_event( "authenticated", $status, $line );
711 0         0 goto_state("ready");
712             }
713 0         0 return;
714             }
715              
716             sub handler_pbsz_prot_failure {
717 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
718              
719 0         0 my $status = substr($input, 0, 3);
720 0         0 my $line = substr($input, 4);
721              
722 0         0 send_event( "login_error",
723             $status, $line );
724 0         0 return;
725             }
726              
727             ## default_simple state
728              
729             # simple commands simply involve a command and one or more responses
730             sub do_simple_command {
731 10     10 0 21 my ($kernel, $heap, $event) = @_[KERNEL, HEAP, STATE];
732              
733 10         26 goto_state("default_simple");
734              
735 10         34 command( [ $event, @_[ARG0..$#_] ] );
736 10         539 return;
737             }
738              
739             # end of response section will be marked by "\d{3} " whereas multipart
740             # messages will be "\d{3}-"
741             sub handler_simple_success {
742 10     10 0 23 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
743              
744 10         16 my $status = substr($input, 0, 3);
745              
746             send_event( lc $heap->{event}->[0],
747             $status, $heap->{ftp_message},
748 10         36 $heap->{event}->[1] );
749              
750 10         23 goto_state("ready");
751 10         10 return;
752             }
753              
754             # server response for failure
755             sub handler_simple_failure {
756 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
757              
758 0         0 my $status = substr($input, 0, 3);
759 0         0 my $line = substr($input, 4);
760              
761             send_event( lc $heap->{event}->[0] . "_error",
762             $status, $line,
763 0         0 $heap->{event}->[1] );
764              
765 0         0 goto_state("ready");
766 0         0 return;
767             }
768              
769             ## default_complex state
770              
771             # complex commands are those which require a data connection
772             sub do_complex_command {
773 3     3 0 11 my ($kernel, $heap, $event) = @_[KERNEL, HEAP, STATE];
774              
775 3         8 goto_state("default_complex");
776              
777 3         14 $heap->{complex_stack} = { command => [ $event, @_[ARG0..$#_] ] };
778              
779 3         9 establish_data_conn();
780 3         3 return;
781             }
782              
783             # use the server response only for data connection establishment
784             # we will know when the command is actually done when the server
785             # terminates the data connection
786             sub handler_complex_success {
787 6     6 0 17 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
788 6 100       31 if ($heap->{event}->[0] eq "PASV") {
    100          
789              
790 2         2 my (@ip, @port);
791 2         17 (@ip[0..3], @port[0..1]) = $input =~ /(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/;
792 2         7 my $ip = join '.', @ip;
793 2         6 my $port = $port[0]*256 + $port[1];
794              
795 2         14 $heap->{data_sock_wheel} = POE::Wheel::SocketFactory->new(
796             SocketDomain => AF_INET,
797             SocketType => SOCK_STREAM,
798             SocketProtocol => 'tcp',
799             RemotePort => $port,
800             RemoteAddress => $ip,
801             SuccessEvent => 'data_connected',
802             FailureEvent => 'data_connect_error'
803             );
804             }
805             elsif ($heap->{event}->[0] =~ /^PORT/) {
806 1         3 command($heap->{complex_stack}->{command});
807             }
808             else {
809 3         18 goto_state("ready");
810             }
811 6         697 return;
812             }
813              
814             # server response for failure
815             sub handler_complex_failure {
816 0     0 0 0 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
817              
818 0         0 my $status = substr($input, 0, 3);
819 0         0 my $line = substr($input, 4);
820              
821             send_event( $heap->{complex_stack}->{command}->[0] . "_error",
822             $status, $line,
823 0         0 $heap->{complex_stack}->{command}->[1] );
824              
825 0         0 clean_up_complex_cmd();
826 0         0 goto_state("ready");
827 0         0 return;
828             }
829              
830             # connection announced by server
831             sub handler_complex_preliminary {
832 3     3 0 7 my ($kernel, $heap) = @_[KERNEL, HEAP];
833              
834             # this message is pretty worthless since _connected is all that matters
835             send_event( $heap->{complex_stack}->{command}->[0] . "_server",
836 3         15 $heap->{complex_stack}->{command}->[1] );
837              
838             # sslify the data connection
839 3         14 my $socket = $heap->{data_rw_wheel}->get_input_handle();
840 3         25 delete $heap->{data_rw_wheel};
841 3 50       451 if ( $heap->{tlsdata} ) {
842 0         0 eval { $socket = Client_SSLify( $socket, 'tlsv1' )};
  0         0  
843 0 0       0 die "Unable to SSLify data connection: $@" if $@;
844             }
845              
846             # set up the rw wheel again
847              
848             $heap->{data_rw_wheel} = POE::Wheel::ReadWrite->new(
849             Handle => $socket,
850             Filter => $heap->{filters}->{ $heap->{complex_stack}->{command}->[0] },
851 3         28 Driver => POE::Driver::SysRW->new( BlockSize => $heap->{attr_blocksize} ),
852             InputEvent => 'data_input',
853             ErrorEvent => 'data_error',
854             FlushedEvent => 'data_flush'
855             );
856              
857 3         577 return;
858             }
859              
860             # data connection established
861             sub handler_complex_connected {
862 3     3 0 9 my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];
863              
864             $heap->{data_rw_wheel} = POE::Wheel::ReadWrite->new(
865             Handle => $socket,
866             Filter => $heap->{filters}->{ $heap->{complex_stack}->{command}->[0] },
867 3         23 Driver => POE::Driver::SysRW->new( BlockSize => $heap->{attr_blocksize} ),
868             InputEvent => 'data_input',
869             ErrorEvent => 'data_error',
870             FlushedEvent => 'data_flush'
871             );
872              
873             send_event( $heap->{complex_stack}->{command}->[0] . "_connected",
874 3         539 $heap->{complex_stack}->{command}->[1] );
875              
876 3 100       11 if ($heap->{attr_conn_mode} == FTP_PASSIVE) {
877 2         5 command($heap->{complex_stack}->{command});
878             }
879 3         97 return;
880             }
881              
882             # data connection could not be established
883             sub handler_complex_connect_error {
884 0     0 0 0 my ($kernel, $heap, $error) = @_[KERNEL, HEAP, ARG0];
885             send_event( $heap->{complex_stack}->{command}->[0] . "_error", $error,
886 0         0 $heap->{complex_stack}->{command}->[1] );
887              
888 0         0 clean_up_complex_cmd();
889 0         0 goto_state("ready");
890 0         0 return;
891             }
892              
893             # getting actual data, so send it to the client
894             sub handler_complex_list_data {
895 9     9 0 14 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
896 9         9 warn "<< $input\n" if DEBUG_DATA;
897              
898             send_event( $heap->{complex_stack}->{command}->[0] . "_data", $input,
899 9         31 $heap->{complex_stack}->{command}->[1] );
900 9         13 return;
901             }
902              
903             # connection was closed, clean up, and wait for a response from the server
904             sub handler_complex_list_error {
905 3     3 0 9 my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
906 3         6 warn "error: complex_list: $input" if DEBUG;
907             send_event( $heap->{complex_stack}->{command}->[0] . "_done",
908 3         14 $heap->{complex_stack}->{command}->[1] );
909              
910 3         32 clean_up_complex_cmd();
911 3         12 dequeue_complex_cmd();
912 3         5 return;
913             }
914              
915             ## utility functions
916              
917             # maps all signal names to dispatcher
918             sub map_all {
919 4     4 0 5 my $map = shift;
920 4         5 my $coderef = shift;
921              
922 4         3 my %signals;
923 4         21 foreach my $state (keys %$map) {
924 44         32 @signals{ keys %{ $map->{$state} } } = ();
  44         166  
925             }
926 4         16 map { $_ = $coderef } values %signals;
  180         121  
927              
928 4         21 return \%signals;
929             }
930              
931             # enqueues and incoming signal
932             sub enqueue_event {
933 3     3 0 10 my ($kernel, $heap, $state) = @_[KERNEL, HEAP, STATE];
934 3         5 warn "|| enqueue $state" if DEBUG;
935              
936 3         6 push @{$heap->{queue}}, [ @_ ];
  3         11  
937              
938             }
939              
940             # dequeue and dispatch next event
941             # in a more general model, this could dequeue the first event
942             # that active session knows how to deal with
943             sub dequeue_event {
944 17     17 0 36 my $heap = $poe_kernel->get_active_session()->get_heap();
945 17 100       57 return unless @{$heap->{queue}};
  17         60  
946              
947 3         11 my $state = $heap->{queue}->[0]->[STATE];
948 3         3 warn "|| dequeue $state" if DEBUG;
949              
950 3         8 dispatch( @{ shift @{$heap->{queue}} } );
  3         8  
  3         22  
951             }
952              
953             # if active session knows how to handle this event, dispatch it to them
954             # if not, enqueue the event
955             sub dispatch {
956 94     94 0 177413 my ($kernel, $heap, $state, $input) = @_[KERNEL, HEAP, STATE, ARG0];
957              
958 94 100 100     331 if ($state eq 'cmd_input' && defined $heap->{data_rw_wheel} ) {
959 3         11 my ($code, $msg) = parse_cmd_string(\$input);
960 3 50       17 if ( substr($code, 0, 1) == 2 ) {
961 0         0 enqueue_complex_cmd(@_);
962 0         0 return;
963             }
964             }
965             my $coderef = ( $state_map->{ $heap->{state} }->{$state} ||
966 94   100     346 $state_map->{global}->{$state} ||
967             \&enqueue_event );
968              
969              
970 94         66 warn "-> $heap->{state}\::$state" if DEBUG;
971 94         91 &{ $coderef }(@_);
  94         179  
972 94         169 return;
973             }
974              
975             # Send events to interested sessions
976             sub send_event {
977 36     36 0 67 my ( $event, @args ) = @_;
978 36         26 warn "<*> $event" if DEBUG;
979              
980 36         83 my $heap = $poe_kernel->get_active_session()->get_heap();
981              
982 36         153 for my $session ( keys %{$heap->{events}} ) {
  36         79  
983 36 100 66     114 if (
984             exists $heap->{events}{$session}{$event} or
985             exists $heap->{events}{$session}{all}
986             )
987             {
988             $poe_kernel->post(
989             $session,
990 29   33     91 ( $heap->{events}{$session}{$event} || $event ),
991             @args
992             );
993             }
994             }
995 36         1581 return;
996             }
997              
998             # run a command and add its call information to the call stack
999             sub command {
1000 24     24 0 36 my ($cmd_args, $state) = @_;
1001              
1002 24 100       66 $cmd_args = ref($cmd_args) eq "ARRAY" ? [ @$cmd_args ] : $cmd_args;
1003              
1004 24         55 my $heap = $poe_kernel->get_active_session()->get_heap();
1005 24 50       114 return unless defined $heap->{cmd_rw_wheel};
1006              
1007 24 100       47 $cmd_args = [$cmd_args] unless ref( $cmd_args ) eq 'ARRAY';
1008 24         48 my $command = uc shift( @$cmd_args );
1009 24 50       51 $state = {} unless defined $state;
1010              
1011 24         17 unshift @{$heap->{stack}}, [ $command, @$cmd_args ];
  24         54  
1012              
1013 24 50       44 $command = shift( @$cmd_args ) if $command eq "QUOT";
1014              
1015 24   66     80 $command = $command_map{$command} || $command;
1016 24 100       60 my $cmdstr = join( ' ', $command, @$cmd_args ? @$cmd_args : () );
1017              
1018 24         25 warn ">>> $cmdstr\n" if DEBUG_COMMAND;
1019              
1020 24         65 $heap->{cmd_rw_wheel}->put($cmdstr);
1021             }
1022              
1023             # change active state
1024             sub goto_state {
1025 38     38 0 44 my $state = shift;
1026 38         30 warn "--> $state" if DEBUG;
1027              
1028 38         70 my $heap = $poe_kernel->get_active_session()->get_heap();
1029 38         171 $heap->{state} = $state;
1030              
1031 38         55 my $coderef = $state_map->{$state}->{_start};
1032 38 100       70 &{$coderef} if $coderef;
  21         34  
1033              
1034             }
1035              
1036             # initiate start of data connection
1037             sub establish_data_conn {
1038 3     3 0 7 my $heap = $poe_kernel->get_active_session()->get_heap();
1039              
1040 3 100       16 if ($heap->{attr_conn_mode} == FTP_PASSIVE) {
1041 2         5 command("PASV");
1042             }
1043             else {
1044             $heap->{data_sock_wheel} = POE::Wheel::SocketFactory->new(
1045             SocketDomain => AF_INET,
1046             SocketType => SOCK_STREAM,
1047             SocketProtocol => 'tcp',
1048             BindAddress => $heap->{local_addr},
1049             BindPort => $heap->{local_port},
1050 1         6 SuccessEvent => 'data_connected',
1051             FailureEvent => 'data_connect_error'
1052             );
1053 1         208 my $socket = $heap->{data_sock_wheel}->getsockname();
1054 1         8 my ($port, $addr) = sockaddr_in($socket);
1055 1         11 $addr = inet_ntoa($addr);
1056 1 50       3 $addr = "127.0.0.1" if $addr eq "0.0.0.0";
1057              
1058 1         4 my @addr = split /\./, $addr;
1059 1         5 my @port = (int($port / 256), $port % 256);
1060 1         8 command("PORT " . join ",", @addr, @port);
1061             }
1062 3         124 return;
1063             }
1064              
1065             sub parse_cmd_string {
1066 50     50 0 46 my $string = shift;
1067 50         155 $$string =~ s/^(\d\d\d)(.?)//o;
1068 50         109 my ($code, $more) = ($1, $2);
1069 50         89 return ($code, $more);
1070             }
1071              
1072             sub clean_up_complex_cmd {
1073 7     7 0 21 my $heap = $poe_kernel->get_active_session()->get_heap();
1074 7         40 delete $heap->{data_sock_wheel};
1075 7         146 delete $heap->{data_rw_wheel};
1076 7         382 $heap->{complex_stack} = {};
1077             }
1078              
1079             sub enqueue_complex_cmd {
1080 0     0 0 0 my $heap = $poe_kernel->get_active_session()->get_heap();
1081 0         0 warn "enqueue_complex_cmd $_[STATE]" if DEBUG;
1082 0         0 $heap->{pending_complex_cmd} = \@_;
1083             }
1084              
1085             sub dequeue_complex_cmd {
1086 3     3 0 9 my $heap = $poe_kernel->get_active_session()->get_heap();
1087 3 50       20 return unless $heap->{pending_complex_cmd};
1088              
1089 0           my $state = $heap->{pending_complex_cmd}->[STATE];
1090 0           warn "dequeue_complex_cmd $state" if DEBUG;
1091 0           my @event = @{ $heap->{pending_complex_cmd} };
  0            
1092 0           $heap->{pending_complex_cmd} = undef;
1093 0           dispatch( @event );
1094             }
1095              
1096             1;
1097              
1098             __END__