2 * Copyright (C) 1996-2016 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
9 /* DEBUG: section 09 File Transfer Protocol (FTP) */
12 #include "anyp/PortCfg.h"
13 #include "client_side.h"
14 #include "clients/forward.h"
15 #include "clients/FtpClient.h"
16 #include "ftp/Elements.h"
17 #include "ftp/Parsing.h"
18 #include "http/Stream.h"
19 #include "HttpHdrCc.h"
20 #include "HttpRequest.h"
21 #include "sbuf/SBuf.h"
22 #include "servers/FtpServer.h"
23 #include "SquidTime.h"
30 /// An FTP client receiving native FTP commands from our FTP server
31 /// (Ftp::Server), forwarding them to the next FTP hop,
32 /// and then relaying FTP replies back to our FTP server.
33 class Relay
: public Ftp::Client
38 explicit Relay(FwdState
*const fwdState
);
42 const Ftp::MasterState
&master() const;
43 Ftp::MasterState
&updateMaster();
44 Ftp::ServerState
serverState() const { return master().serverState
; }
45 void serverState(const Ftp::ServerState newState
);
48 virtual void failed(err_type error
= ERR_NONE
, int xerrno
= 0, ErrorState
*ftperr
= nullptr);
49 virtual void dataChannelConnected(const CommConnectCbParams
&io
);
52 virtual void serverComplete();
53 virtual void handleControlReply();
54 virtual void processReplyBody();
55 virtual void handleRequestBodyProducerAborted();
56 virtual bool mayReadVirginReplyBody() const;
57 virtual void completeForwarding();
58 virtual bool abortOnData(const char *reason
);
62 virtual void swanSong();
65 void forwardError(err_type error
= ERR_NONE
, int xerrno
= 0);
66 void failedErrorMessage(err_type error
, int xerrno
);
67 HttpReply
*createHttpReply(const Http::StatusCode httpStatus
, const int64_t clen
= 0);
68 void handleDataRequest();
69 void startDataDownload();
70 void startDataUpload();
71 bool startDirTracking();
72 void stopDirTracking();
73 bool weAreTrackingDir() const {return savedReply
.message
!= NULL
;}
75 typedef void (Relay::*PreliminaryCb
)();
76 void forwardPreliminaryReply(const PreliminaryCb cb
);
77 void proceedAfterPreliminaryReply();
78 PreliminaryCb thePreliminaryCb
;
80 typedef void (Relay::*SM_FUNC
)();
81 static const SM_FUNC SM_FUNCS
[];
88 void readTransferDoneReply();
90 void readCwdOrCdupReply();
91 void readUserOrPassReply();
93 void scheduleReadControlReply();
95 /// Inform Ftp::Server that we are done if originWaitInProgress
96 void stopOriginWait(int code
);
98 static void abort(void *d
); // TODO: Capitalize this and FwdState::abort().
100 bool forwardingCompleted
; ///< completeForwarding() has been called
102 /// whether we are between Ftp::Server::startWaitingForOrigin() and
103 /// Ftp::Server::stopWaitingForOrigin() calls
104 bool originWaitInProgress
;
107 wordlist
*message
; ///< reply message, one wordlist entry per message line
108 char *lastCommand
; ///< the command caused the reply
109 char *lastReply
; ///< last line of reply: reply status plus message
110 int replyCode
; ///< the reply status
111 } savedReply
; ///< set and delayed while we are tracking using PWD
116 CBDATA_NAMESPACED_CLASS_INIT(Ftp
, Relay
);
118 const Ftp::Relay::SM_FUNC
Ftp::Relay::SM_FUNCS
[] = {
119 &Ftp::Relay::readGreeting
, // BEGIN
120 &Ftp::Relay::readUserOrPassReply
, // SENT_USER
121 &Ftp::Relay::readUserOrPassReply
, // SENT_PASS
122 NULL
,/* &Ftp::Relay::readReply */ // SENT_TYPE
123 NULL
,/* &Ftp::Relay::readReply */ // SENT_MDTM
124 NULL
,/* &Ftp::Relay::readReply */ // SENT_SIZE
127 &Ftp::Relay::readEpsvReply
, // SENT_EPSV_ALL
128 &Ftp::Relay::readEpsvReply
, // SENT_EPSV_1
129 &Ftp::Relay::readEpsvReply
, // SENT_EPSV_2
130 &Ftp::Relay::readPasvReply
, // SENT_PASV
131 &Ftp::Relay::readCwdOrCdupReply
, // SENT_CWD
132 NULL
,/* &Ftp::Relay::readDataReply, */ // SENT_LIST
133 NULL
,/* &Ftp::Relay::readDataReply, */ // SENT_NLST
134 NULL
,/* &Ftp::Relay::readReply */ // SENT_REST
135 NULL
,/* &Ftp::Relay::readDataReply */ // SENT_RETR
136 NULL
,/* &Ftp::Relay::readReply */ // SENT_STOR
137 NULL
,/* &Ftp::Relay::readReply */ // SENT_QUIT
138 &Ftp::Relay::readTransferDoneReply
, // READING_DATA
139 &Ftp::Relay::readReply
, // WRITING_DATA
140 NULL
,/* &Ftp::Relay::readReply */ // SENT_MKDIR
141 &Ftp::Relay::readFeatReply
, // SENT_FEAT
142 NULL
,/* &Ftp::Relay::readPwdReply */ // SENT_PWD
143 &Ftp::Relay::readCwdOrCdupReply
, // SENT_CDUP
144 &Ftp::Relay::readDataReply
,// SENT_DATA_REQUEST
145 &Ftp::Relay::readReply
, // SENT_COMMAND
149 Ftp::Relay::Relay(FwdState
*const fwdState
):
150 AsyncJob("Ftp::Relay"),
151 Ftp::Client(fwdState
),
152 thePreliminaryCb(NULL
),
153 forwardingCompleted(false),
154 originWaitInProgress(false)
156 savedReply
.message
= NULL
;
157 savedReply
.lastCommand
= NULL
;
158 savedReply
.lastReply
= NULL
;
159 savedReply
.replyCode
= 0;
161 // Nothing we can do at request creation time can mark the response as
162 // uncachable, unfortunately. This prevents "found KEY_PRIVATE" WARNINGs.
163 entry
->releaseRequest();
164 // TODO: Convert registerAbort() to use AsyncCall
165 entry
->registerAbort(Ftp::Relay::abort
, this);
170 closeServer(); // TODO: move to clients/Client.cc?
171 if (savedReply
.message
)
172 wordlistDestroy(&savedReply
.message
);
174 xfree(savedReply
.lastCommand
);
175 xfree(savedReply
.lastReply
);
181 if (!master().clientReadGreeting
)
182 Ftp::Client::start();
183 else if (serverState() == fssHandleDataRequest
||
184 serverState() == fssHandleUploadRequest
)
191 Ftp::Relay::swanSong()
194 Ftp::Client::swanSong();
197 /// Keep control connection for future requests, after we are done with it.
198 /// Similar to COMPLETE_PERSISTENT_MSG handling in http.cc.
200 Ftp::Relay::serverComplete()
202 stopOriginWait(ctrl
.replycode
);
204 CbcPointer
<ConnStateData
> &mgr
= fwd
->request
->clientConnectionManager
;
206 if (Comm::IsConnOpen(ctrl
.conn
)) {
207 debugs(9, 7, "completing FTP server " << ctrl
.conn
<<
208 " after " << ctrl
.replycode
);
209 fwd
->unregister(ctrl
.conn
);
210 if (ctrl
.replycode
== 221) { // Server sends FTP 221 before closing
211 mgr
->unpinConnection(false);
214 mgr
->pinConnection(ctrl
.conn
, fwd
->request
,
215 ctrl
.conn
->getPeer(),
216 fwd
->request
->flags
.connectionAuth
);
221 Ftp::Client::serverComplete();
224 /// Safely returns the master state,
225 /// with safety checks in case the Ftp::Server side of the master xact is gone.
227 Ftp::Relay::updateMaster()
229 CbcPointer
<ConnStateData
> &mgr
= fwd
->request
->clientConnectionManager
;
231 if (Ftp::Server
*srv
= dynamic_cast<Ftp::Server
*>(mgr
.get()))
234 // this code will not be necessary once the master is inside MasterXaction
235 debugs(9, 3, "our server side is gone: " << mgr
);
236 static Ftp::MasterState Master
;
237 Master
= Ftp::MasterState();
241 /// A const variant of updateMaster().
242 const Ftp::MasterState
&
243 Ftp::Relay::master() const
245 return const_cast<Ftp::Relay
*>(this)->updateMaster(); // avoid code dupe
248 /// Changes server state and debugs about that important event.
250 Ftp::Relay::serverState(const Ftp::ServerState newState
)
252 Ftp::ServerState
&cltState
= updateMaster().serverState
;
253 debugs(9, 3, "client state was " << cltState
<< " now: " << newState
);
258 * Ensure we do not double-complete on the forward entry.
259 * We complete forwarding when the response adaptation is over
260 * (but we may still be waiting for 226 from the FTP server) and
261 * also when we get that 226 from the server (and adaptation is done).
263 \todo Rewrite FwdState to ignore double completion?
266 Ftp::Relay::completeForwarding()
268 debugs(9, 5, forwardingCompleted
);
269 if (forwardingCompleted
)
271 forwardingCompleted
= true;
272 Ftp::Client::completeForwarding();
276 Ftp::Relay::failed(err_type error
, int xerrno
, ErrorState
*ftpErr
)
278 if (!doneWithServer())
279 serverState(fssError
);
281 // TODO: we need to customize ErrorState instead
282 if (entry
->isEmpty())
283 failedErrorMessage(error
, xerrno
); // as a reply
285 Ftp::Client::failed(error
, xerrno
, ftpErr
);
289 Ftp::Relay::failedErrorMessage(err_type error
, int xerrno
)
291 const Http::StatusCode httpStatus
= failedHttpStatus(error
);
292 HttpReply
*const reply
= createHttpReply(httpStatus
);
293 entry
->replaceHttpReply(reply
);
294 EBIT_CLR(entry
->flags
, ENTRY_FWD_HDR_WAIT
);
295 fwd
->request
->detailError(error
, xerrno
);
299 Ftp::Relay::processReplyBody()
301 debugs(9, 3, status());
303 if (EBIT_TEST(entry
->flags
, ENTRY_ABORTED
)) {
305 * probably was aborted because content length exceeds one
306 * of the maximum size limits.
308 abortOnData("entry aborted after calling appendSuccessHeader()");
312 if (master().userDataDone
) {
313 // Squid-to-client data transfer done. Abort data transfer on our
314 // side to allow new commands from ftp client
315 abortOnData("Squid-to-client data connection is closed");
321 if (adaptationAccessCheckPending
) {
322 debugs(9, 3, "returning due to adaptationAccessCheckPending");
328 if (data
.readBuf
!= NULL
&& data
.readBuf
->hasContent()) {
329 const mb_size_t csize
= data
.readBuf
->contentSize();
330 debugs(9, 5, "writing " << csize
<< " bytes to the reply");
331 addVirginReplyBody(data
.readBuf
->content(), csize
);
332 data
.readBuf
->consume(csize
);
337 maybeReadVirginBody();
341 Ftp::Relay::handleControlReply()
343 if (!request
->clientConnectionManager
.valid()) {
344 debugs(9, 5, "client connection gone");
349 Ftp::Client::handleControlReply();
350 if (ctrl
.message
== NULL
)
351 return; // didn't get complete reply yet
354 assert(this->SM_FUNCS
[state
] != NULL
);
355 (this->*SM_FUNCS
[state
])();
359 Ftp::Relay::handleRequestBodyProducerAborted()
361 ::Client::handleRequestBodyProducerAborted();
363 failed(ERR_READ_ERROR
);
367 Ftp::Relay::mayReadVirginReplyBody() const
369 // TODO: move this method to the regular FTP server?
370 return Comm::IsConnOpen(data
.conn
);
374 Ftp::Relay::forwardReply()
376 assert(entry
->isEmpty());
377 EBIT_CLR(entry
->flags
, ENTRY_FWD_HDR_WAIT
);
379 HttpReply
*const reply
= createHttpReply(Http::scNoContent
);
380 reply
->sources
|= HttpMsg::srcFtp
;
382 setVirginReply(reply
);
383 adaptOrFinalizeReply();
389 Ftp::Relay::forwardPreliminaryReply(const PreliminaryCb cb
)
391 debugs(9, 5, "forwarding preliminary reply to client");
393 // we must prevent concurrent ConnStateData::sendControlMsg() calls
394 Must(thePreliminaryCb
== NULL
);
395 thePreliminaryCb
= cb
;
397 const HttpReply::Pointer reply
= createHttpReply(Http::scContinue
);
399 // the Sink will use this to call us back after writing 1xx to the client
400 typedef NullaryMemFunT
<Relay
> CbDialer
;
401 const AsyncCall::Pointer call
= JobCallback(11, 3, CbDialer
, this,
402 Ftp::Relay::proceedAfterPreliminaryReply
);
404 CallJobHere1(9, 4, request
->clientConnectionManager
, ConnStateData
,
405 ConnStateData::sendControlMsg
, HttpControlMsg(reply
, call
));
409 Ftp::Relay::proceedAfterPreliminaryReply()
411 debugs(9, 5, "proceeding after preliminary reply to client");
413 Must(thePreliminaryCb
!= NULL
);
414 const PreliminaryCb cb
= thePreliminaryCb
;
415 thePreliminaryCb
= NULL
;
420 Ftp::Relay::forwardError(err_type error
, int xerrno
)
422 failed(error
, xerrno
);
426 Ftp::Relay::createHttpReply(const Http::StatusCode httpStatus
, const int64_t clen
)
428 HttpReply
*const reply
= Ftp::HttpReplyWrapper(ctrl
.replycode
, ctrl
.last_reply
, httpStatus
, clen
);
430 for (wordlist
*W
= ctrl
.message
; W
&& W
->next
; W
= W
->next
)
431 reply
->header
.putStr(Http::HdrType::FTP_PRE
, httpHeaderQuoteString(W
->key
).c_str());
432 // no hdrCacheInit() is needed for after Http::HdrType::FTP_PRE addition
438 Ftp::Relay::handleDataRequest()
440 data
.addr(master().clientDataAddr
);
441 connectDataChannel();
445 Ftp::Relay::startDataDownload()
447 assert(Comm::IsConnOpen(data
.conn
));
449 debugs(9, 3, "begin data transfer from " << data
.conn
->remote
<<
450 " (" << data
.conn
->local
<< ")");
452 HttpReply
*const reply
= createHttpReply(Http::scOkay
, -1);
453 reply
->sources
|= HttpMsg::srcFtp
;
455 EBIT_CLR(entry
->flags
, ENTRY_FWD_HDR_WAIT
);
456 setVirginReply(reply
);
457 adaptOrFinalizeReply();
459 maybeReadVirginBody();
460 state
= READING_DATA
;
464 Ftp::Relay::startDataUpload()
466 assert(Comm::IsConnOpen(data
.conn
));
468 debugs(9, 3, "begin data transfer to " << data
.conn
->remote
<<
469 " (" << data
.conn
->local
<< ")");
471 if (!startRequestBodyFlow()) { // register to receive body data
476 state
= WRITING_DATA
;
480 Ftp::Relay::readGreeting()
482 assert(!master().clientReadGreeting
);
484 switch (ctrl
.replycode
) {
486 updateMaster().clientReadGreeting
= true;
487 if (serverState() == fssBegin
)
488 serverState(fssConnected
);
490 // Do not forward server greeting to the user because our FTP Server
491 // has greeted the user already. Also, an original origin greeting may
492 // confuse a user that has changed the origin mid-air.
497 if (NULL
!= ctrl
.message
)
498 debugs(9, DBG_IMPORTANT
, "FTP server is busy: " << ctrl
.message
->key
);
499 forwardPreliminaryReply(&Ftp::Relay::scheduleReadControlReply
);
508 Ftp::Relay::sendCommand()
510 if (!fwd
->request
->header
.has(Http::HdrType::FTP_COMMAND
)) {
511 abortAll("Internal error: FTP relay request with no command");
515 HttpHeader
&header
= fwd
->request
->header
;
516 assert(header
.has(Http::HdrType::FTP_COMMAND
));
517 const String
&cmd
= header
.findEntry(Http::HdrType::FTP_COMMAND
)->value
;
518 assert(header
.has(Http::HdrType::FTP_ARGUMENTS
));
519 const String
¶ms
= header
.findEntry(Http::HdrType::FTP_ARGUMENTS
)->value
;
521 if (params
.size() > 0)
522 debugs(9, 5, "command: " << cmd
<< ", parameters: " << params
);
524 debugs(9, 5, "command: " << cmd
<< ", no parameters");
526 if (serverState() == fssHandlePasv
||
527 serverState() == fssHandleEpsv
||
528 serverState() == fssHandleEprt
||
529 serverState() == fssHandlePort
) {
535 if (params
.size() > 0)
536 buf
.Printf("%s %s%s", cmd
.termedBuf(), params
.termedBuf(), Ftp::crlf
);
538 buf
.Printf("%s%s", cmd
.termedBuf(), Ftp::crlf
);
540 writeCommand(buf
.c_str());
543 serverState() == fssHandleCdup
? SENT_CDUP
:
544 serverState() == fssHandleCwd
? SENT_CWD
:
545 serverState() == fssHandleFeat
? SENT_FEAT
:
546 serverState() == fssHandleDataRequest
? SENT_DATA_REQUEST
:
547 serverState() == fssHandleUploadRequest
? SENT_DATA_REQUEST
:
548 serverState() == fssConnected
? SENT_USER
:
549 serverState() == fssHandlePass
? SENT_PASS
:
552 if (state
== SENT_DATA_REQUEST
) {
553 CbcPointer
<ConnStateData
> &mgr
= fwd
->request
->clientConnectionManager
;
555 if (Ftp::Server
*srv
= dynamic_cast<Ftp::Server
*>(mgr
.get())) {
556 typedef NullaryMemFunT
<Ftp::Server
> CbDialer
;
557 AsyncCall::Pointer call
= JobCallback(11, 3, CbDialer
, srv
,
558 Ftp::Server::startWaitingForOrigin
);
559 ScheduleCallHere(call
);
560 originWaitInProgress
= true;
567 Ftp::Relay::readReply()
569 assert(serverState() == fssConnected
||
570 serverState() == fssHandleUploadRequest
);
572 if (100 <= ctrl
.replycode
&& ctrl
.replycode
< 200)
573 forwardPreliminaryReply(&Ftp::Relay::scheduleReadControlReply
);
579 Ftp::Relay::readFeatReply()
581 assert(serverState() == fssHandleFeat
);
583 if (100 <= ctrl
.replycode
&& ctrl
.replycode
< 200)
584 return; // ignore preliminary replies
590 Ftp::Relay::readPasvReply()
592 assert(serverState() == fssHandlePasv
|| serverState() == fssHandleEpsv
|| serverState() == fssHandlePort
|| serverState() == fssHandleEprt
);
594 if (100 <= ctrl
.replycode
&& ctrl
.replycode
< 200)
595 return; // ignore preliminary replies
597 if (handlePasvReply(updateMaster().clientDataAddr
))
604 Ftp::Relay::readEpsvReply()
606 if (100 <= ctrl
.replycode
&& ctrl
.replycode
< 200)
607 return; // ignore preliminary replies
609 if (handleEpsvReply(updateMaster().clientDataAddr
)) {
610 if (ctrl
.message
== NULL
)
611 return; // didn't get complete reply yet
619 Ftp::Relay::readDataReply()
621 assert(serverState() == fssHandleDataRequest
||
622 serverState() == fssHandleUploadRequest
);
624 if (ctrl
.replycode
== 125 || ctrl
.replycode
== 150) {
625 if (serverState() == fssHandleDataRequest
)
626 forwardPreliminaryReply(&Ftp::Relay::startDataDownload
);
627 else if (fwd
->request
->forcedBodyContinuation
/*&& serverState() == fssHandleUploadRequest*/)
629 else // serverState() == fssHandleUploadRequest
630 forwardPreliminaryReply(&Ftp::Relay::startDataUpload
);
636 Ftp::Relay::startDirTracking()
638 if (!fwd
->request
->clientConnectionManager
->port
->ftp_track_dirs
)
641 debugs(9, 5, "start directory tracking");
642 savedReply
.message
= ctrl
.message
;
643 savedReply
.lastCommand
= ctrl
.last_command
;
644 savedReply
.lastReply
= ctrl
.last_reply
;
645 savedReply
.replyCode
= ctrl
.replycode
;
647 ctrl
.last_command
= NULL
;
648 ctrl
.last_reply
= NULL
;
651 writeCommand("PWD\r\n");
656 Ftp::Relay::stopDirTracking()
658 debugs(9, 5, "got code from pwd: " << ctrl
.replycode
<< ", msg: " << ctrl
.last_reply
);
660 if (ctrl
.replycode
== 257)
661 updateMaster().workingDir
= Ftp::UnescapeDoubleQuoted(ctrl
.last_reply
);
663 wordlistDestroy(&ctrl
.message
);
664 safe_free(ctrl
.last_command
);
665 safe_free(ctrl
.last_reply
);
667 ctrl
.message
= savedReply
.message
;
668 ctrl
.last_command
= savedReply
.lastCommand
;
669 ctrl
.last_reply
= savedReply
.lastReply
;
670 ctrl
.replycode
= savedReply
.replyCode
;
672 savedReply
.message
= NULL
;
673 savedReply
.lastReply
= NULL
;
674 savedReply
.lastCommand
= NULL
;
678 Ftp::Relay::readCwdOrCdupReply()
680 assert(serverState() == fssHandleCwd
||
681 serverState() == fssHandleCdup
);
683 debugs(9, 5, "got code " << ctrl
.replycode
<< ", msg: " << ctrl
.last_reply
);
685 if (100 <= ctrl
.replycode
&& ctrl
.replycode
< 200)
688 if (weAreTrackingDir()) { // we are tracking
689 stopDirTracking(); // and forward the delayed response below
690 } else if (startDirTracking())
697 Ftp::Relay::readUserOrPassReply()
699 if (100 <= ctrl
.replycode
&& ctrl
.replycode
< 200)
700 return; //Just ignore
702 if (weAreTrackingDir()) { // we are tracking
703 stopDirTracking(); // and forward the delayed response below
704 } else if (ctrl
.replycode
== 230) { // successful login
705 if (startDirTracking())
713 Ftp::Relay::readTransferDoneReply()
715 debugs(9, 3, status());
717 if (ctrl
.replycode
!= 226 && ctrl
.replycode
!= 250) {
718 debugs(9, DBG_IMPORTANT
, "got FTP code " << ctrl
.replycode
<<
719 " after reading response data");
722 debugs(9, 2, "Complete data downloading");
728 Ftp::Relay::dataChannelConnected(const CommConnectCbParams
&io
)
730 debugs(9, 3, status());
733 if (io
.flag
!= Comm::OK
) {
734 debugs(9, 2, "failed to connect FTP server data channel");
735 forwardError(ERR_CONNECT_FAIL
, io
.xerrno
);
739 debugs(9, 2, "connected FTP server data channel: " << io
.conn
);
741 data
.opened(io
.conn
, dataCloser());
747 Ftp::Relay::scheduleReadControlReply()
749 Ftp::Client::scheduleReadControlReply(0);
753 Ftp::Relay::abortOnData(const char *reason
)
755 debugs(9, 3, "aborting transaction for " << reason
<<
756 "; FD " << (ctrl
.conn
!= NULL
? ctrl
.conn
->fd
: -1) << ", Data FD " << (data
.conn
!= NULL
? data
.conn
->fd
: -1) << ", this " << this);
757 // this method is only called to handle data connection problems
758 // the control connection should keep going
761 if (adaptedBodySource
!= NULL
)
762 stopConsumingFrom(adaptedBodySource
);
765 if (Comm::IsConnOpen(data
.conn
))
768 return !Comm::IsConnOpen(ctrl
.conn
);
772 Ftp::Relay::stopOriginWait(int code
)
774 if (originWaitInProgress
) {
775 CbcPointer
<ConnStateData
> &mgr
= fwd
->request
->clientConnectionManager
;
777 if (Ftp::Server
*srv
= dynamic_cast<Ftp::Server
*>(mgr
.get())) {
778 typedef UnaryMemFunT
<Ftp::Server
, int> CbDialer
;
779 AsyncCall::Pointer call
= asyncCall(11, 3, "Ftp::Server::stopWaitingForOrigin",
780 CbDialer(srv
, &Ftp::Server::stopWaitingForOrigin
, code
));
781 ScheduleCallHere(call
);
784 originWaitInProgress
= false;
789 Ftp::Relay::abort(void *d
)
791 Ftp::Relay
*ftpClient
= (Ftp::Relay
*)d
;
792 debugs(9, 2, "Client Data connection closed!");
793 if (!cbdataReferenceValid(ftpClient
))
795 if (Comm::IsConnOpen(ftpClient
->data
.conn
))
796 ftpClient
->dataComplete();
800 Ftp::StartRelay(FwdState
*const fwdState
)
802 return AsyncJob::Start(new Ftp::Relay(fwdState
));