File Coverage

blib/lib/POE/Component/Client/FTP.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


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