// We hope that either the store entry aborts or peer is selected.
// Otherwise we are going to leak our object.
- entry->registerAbort(FwdState::abort, this);
+ // Ftp::Relay needs to preserve control connection on data aborts
+ // so it registers its own abort handler that calls ours when needed.
+ if (!request->flags.ftpNative)
+ entry->registerAbort(FwdState::abort, this);
#if STRICT_ORIGINAL_DST
// Bug 3243: CVE 2009-0801
return false;
debugs(11,5, HERE << "entry is not Accepting!");
- abortTransaction(abortReason);
+ abortOnData(abortReason);
return true;
}
handleRequestBodyProducerAborted();
}
+bool
+Client::abortOnData(const char *reason)
+{
+ abortAll(reason);
+ return true;
+}
+
// more origin request body data is available
void
Client::handleMoreRequestBodyAvailable()
err = new ErrorState(ERR_WRITE_ERROR, Http::scBadGateway, fwd->request);
err->xerrno = io.xerrno;
fwd->fail(err);
- abortTransaction("I/O error while sending request body");
+ abortOnData("I/O error while sending request body");
return;
}
if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
- abortTransaction("store entry aborted while sending request body");
+ abortOnData("store entry aborted while sending request body");
return;
}
// TODO: bypass if possible
if (!handledEarlyAdaptationAbort())
- abortTransaction("adaptation failure with a filled entry");
+ abortAll("adaptation failure with a filled entry");
}
/// If the store entry is still empty, fully handles adaptation abort, returning
err->detailError(ERR_DETAIL_ICAP_RESPMOD_EARLY);
fwd->fail(err);
fwd->dontRetry(true);
- abortTransaction("adaptation failure with an empty entry");
+ abortAll("adaptation failure with an empty entry");
return true; // handled
}
if (!entry->isEmpty()) { // too late to block (should not really happen)
if (request)
request->detailError(ERR_ICAP_FAILURE, ERR_DETAIL_RESPMOD_BLOCK_LATE);
- abortTransaction("late adaptation block");
+ abortAll("late adaptation block");
return;
}
fwd->fail(err);
fwd->dontRetry(true);
- abortTransaction("timely adaptation block");
+ abortOnData("timely adaptation block");
}
void
ErrorState *err = new ErrorState(ERR_TOO_BIG, Http::scForbidden, request);
fwd->fail(err);
fwd->dontRetry(true);
- abortTransaction("Virgin body too large.");
+ abortOnData("Virgin body too large.");
}
// TODO: when HttpStateData sends all errors to ICAP,
virtual void maybeReadVirginBody() = 0;
/// abnormal transaction termination; reason is for debugging only
- virtual void abortTransaction(const char *reason) = 0;
+ virtual void abortAll(const char *reason) = 0;
+
+ /// abnormal data transfer termination
+ /// \retval true the transaction will be terminated (abortAll called)
+ /// \reval false the transaction will survive
+ virtual bool abortOnData(const char *reason);
/// a hack to reach HttpStateData::orignal_request
virtual HttpRequest *originalRequest();
return;
if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
- abortTransaction("entry aborted during control reply read");
- return;
+ if (abortOnData("entry aborted during control reply read"))
+ return;
}
assert(ctrl.offset < ctrl.size);
}
/* XXX this may end up having to be serverComplete() .. */
- abortTransaction("zero control reply read");
+ abortAll("zero control reply read");
return;
}
assert(io.fd == data.conn->fd);
if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
- abortTransaction("entry aborted during dataRead");
+ abortOnData("entry aborted during dataRead");
return;
}
* including canceling close handlers
*/
void
-Ftp::Client::abortTransaction(const char *reason)
+Ftp::Client::abortAll(const char *reason)
{
debugs(9, 3, "aborting transaction for " << reason <<
"; FD " << (ctrl.conn!=NULL?ctrl.conn->fd:-1) << ", Data FD " << (data.conn!=NULL?data.conn->fd:-1) << ", this " << this);
virtual void closeServer();
virtual bool doneWithServer() const;
virtual const Comm::ConnectionPointer & dataConnection() const;
- virtual void abortTransaction(const char *reason);
+ virtual void abortAll(const char *reason);
virtual Http::StatusCode failedHttpStatus(err_type &error);
void ctrlClosed(const CommCloseCbParams &io);
* probably was aborted because content length exceeds one
* of the maximum size limits.
*/
- abortTransaction("entry aborted after calling appendSuccessHeader()");
+ abortAll("entry aborted after calling appendSuccessHeader()");
return;
}
* trying to write to the client.
*/
if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
- abortTransaction("entry aborted while processing HEAD");
+ abortAll("entry aborted while processing HEAD");
return;
}
debugs(9, 3, HERE);
if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
- abortTransaction("entry aborted when accepting data conn");
+ abortAll("entry aborted when accepting data conn");
data.listenConn->close();
data.listenConn = NULL;
return;
virtual void handleRequestBodyProducerAborted();
virtual bool mayReadVirginReplyBody() const;
virtual void completeForwarding();
+ virtual bool abortOnData(const char *reason);
/* AsyncJob API */
virtual void start();
void readUserOrPassReply();
void scheduleReadControlReply();
+ void finalizeDataDownload();
+
+ static void abort(void *d); // TODO: Capitalize this and FwdState::abort().
bool forwardingCompleted; ///< completeForwarding() has been called
// Nothing we can do at request creation time can mark the response as
// uncachable, unfortunately. This prevents "found KEY_PRIVATE" WARNINGs.
entry->releaseRequest();
+ // TODO: Convert registerAbort() to use AsyncCall
+ entry->registerAbort(Ftp::Relay::abort, this);
}
Ftp::Relay::~Relay()
* probably was aborted because content length exceeds one
* of the maximum size limits.
*/
- abortTransaction("entry aborted after calling appendSuccessHeader()");
+ abortOnData("entry aborted after calling appendSuccessHeader()");
+ return;
+ }
+
+ if (master().userDataDone) {
+ // Squid-to-client data transfer done. Abort data transfer on our
+ // side to allow new commands from ftp client
+ abortOnData("Squid-to-client data connection is closed");
return;
}
Ftp::Relay::sendCommand()
{
if (!fwd->request->header.has(Http::HdrType::FTP_COMMAND)) {
- abortTransaction("Internal error: FTP relay request with no command");
+ abortAll("Internal error: FTP relay request with no command");
return;
}
" after reading response data");
}
- serverComplete();
+ finalizeDataDownload();
}
void
Ftp::Client::scheduleReadControlReply(0);
}
+void
+Ftp::Relay::finalizeDataDownload()
+{
+ debugs(9, 2, "Complete data downloading/Uploading");
+
+ updateMaster().waitForOriginData = false;
+
+ CbcPointer<ConnStateData> &mgr = fwd->request->clientConnectionManager;
+ if (mgr.valid()) {
+ if (Ftp::Server *srv = dynamic_cast<Ftp::Server*>(mgr.get())) {
+ typedef NullaryMemFunT<Ftp::Server> CbDialer;
+ AsyncCall::Pointer call = JobCallback(11, 3, CbDialer, srv,
+ Ftp::Server::originDataCompletionCheckpoint);
+ ScheduleCallHere(call);
+ }
+ }
+ serverComplete();
+}
+
+bool
+Ftp::Relay::abortOnData(const char *reason)
+{
+ debugs(9, 3, "aborting transaction for " << reason <<
+ "; FD " << (ctrl.conn != NULL ? ctrl.conn->fd : -1) << ", Data FD " << (data.conn != NULL ? data.conn->fd : -1) << ", this " << this);
+ // this method is only called to handle data connection problems
+ // the control connection should keep going
+
+#if USE_ADAPTATION
+ if (adaptedBodySource != NULL)
+ stopConsumingFrom(adaptedBodySource);
+#endif
+
+ if (Comm::IsConnOpen(data.conn))
+ dataComplete();
+
+ return !Comm::IsConnOpen(ctrl.conn);
+}
+
+void
+Ftp::Relay::abort(void *d)
+{
+ Ftp::Relay *ftpClient = (Ftp::Relay *)d;
+ debugs(9, 2, "Client Data connection closed!");
+ if (!cbdataReferenceValid(ftpClient))
+ return;
+ if (Comm::IsConnOpen(ftpClient->data.conn))
+ ftpClient->dataComplete();
+}
+
AsyncJob::Pointer
Ftp::StartRelay(FwdState *const fwdState)
{
// TODO: destruction should be sufficient as the destructor should cleanup,
// including canceling close handlers
void
-HttpStateData::abortTransaction(const char *reason)
+HttpStateData::abortAll(const char *reason)
{
debugs(11,5, HERE << "aborting transaction for " << reason <<
"; " << serverConnection << ", this " << this);
}
fwd->handleUnregisteredServerEnd();
- mustStop("HttpStateData::abortTransaction");
+ mustStop("HttpStateData::abortAll");
}
virtual bool getMoreRequestBody(MemBuf &buf);
virtual void closeServer(); // end communication with the server
virtual bool doneWithServer() const; // did we end communication?
- virtual void abortTransaction(const char *reason); // abnormal termination
+ virtual void abortAll(const char *reason); // abnormal termination
virtual bool mayReadVirginReplyBody() const;
+ void abortTransaction(const char *reason) { abortAll(reason); } // abnormal termination
+
/**
* determine if read buffer can have space made available
* for a read.
{
ConnStateData::clientPinnedConnectionClosed(io);
- // if the server control connection is gone, reset state to login again
- resetLogin("control connection closure");
+ // TODO: Keep the control connection open after fixing the reset
+ // problem below
+ if (Comm::IsConnOpen(clientConnection))
+ clientConnection->close();
- // XXX: Reseting is not enough. FtpRelay::sendCommand() will not re-login
- // because FtpRelay::serverState() is not going to be fssConnected.
+ // TODO: If the server control connection is gone, reset state to login
+ // again. Reseting login alone is not enough: FtpRelay::sendCommand() will
+ // not re-login because FtpRelay::serverState() is not going to be
+ // fssConnected. Calling resetLogin() alone is also harmful because
+ // it does not reset correctly the client-to-squid control connection (eg
+ // respond if required with an error code, in all cases)
+ // resetLogin("control connection closure");
}
/// clear client and server login-related state after the old login is gone
if (io.flag != Comm::OK) {
debugs(33, 3, "FTP reply data writing failed: " << xstrerr(io.xerrno));
- closeDataConnection();
- writeCustomReply(426, "Data connection error; transfer aborted");
+ userDataCompletionCheckpoint(426);
return;
}
return;
case STREAM_COMPLETE:
debugs(33, 3, "FTP reply data transfer successfully complete");
- writeCustomReply(226, "Transfer complete");
+ userDataCompletionCheckpoint(226);
break;
case STREAM_UNPLANNED_COMPLETE:
debugs(33, 3, "FTP reply data transfer failed: STREAM_UNPLANNED_COMPLETE");
- writeCustomReply(451, "Server error; transfer aborted");
+ userDataCompletionCheckpoint(451);
break;
case STREAM_FAILED:
+ userDataCompletionCheckpoint(451);
debugs(33, 3, "FTP reply data transfer failed: STREAM_FAILED");
- writeCustomReply(451, "Server error; transfer aborted");
break;
default:
fatal("unreachable code");
}
-
- closeDataConnection();
}
void
if (!checkDataConnPre())
return false;
+ master->waitForOriginData = true;
+ master->userDataDone = 0;
+
changeState(fssHandleDataRequest, "handleDataRequest");
return true;
if (!checkDataConnPre())
return false;
+ master->waitForOriginData = true;
+ master->userDataDone = 0;
+
if (Config.accessList.forceRequestBodyContinuation) {
ClientHttpRequest *http = pipeline.front()->http;
HttpRequest *request = http->request;
AsyncJob::callException(e);
}
+void
+Ftp::Server::originDataCompletionCheckpoint()
+{
+ if (!master->userDataDone) {
+ debugs(33, 5, "Transfering from/to client not finished yet");
+ return;
+ }
+
+ completeDataExchange();
+}
+
+void Ftp::Server::userDataCompletionCheckpoint(int finalStatusCode)
+{
+ Must(!master->userDataDone);
+ master->userDataDone = finalStatusCode;
+
+ if (bodyParser)
+ finishDechunkingRequest(false);
+
+ // The origin control connection is gone, nothing to wait for
+ if (!Comm::IsConnOpen(pinning.serverConnection))
+ master->waitForOriginData = false;
+
+ if (master->waitForOriginData) {
+ // The completeDataExchange() is not called here unconditionally
+ // because we want to signal the FTP user that we are not fully
+ // done processing its data stream, even though all data bytes
+ // have been sent or received already.
+ debugs(33, 5, "Transfering from/to FTP server is not complete");
+ return;
+ }
+
+ completeDataExchange();
+}
+
+void Ftp::Server::completeDataExchange()
+{
+ writeCustomReply(master->userDataDone, master->userDataDone == 226 ? "Transfer complete" : "Server error; transfer aborted");
+ closeDataConnection();
+}
+
/// Whether Squid FTP Relay supports a named feature (e.g., a command).
static bool
Ftp::SupportedCommand(const SBuf &name)
public:
typedef RefCount<MasterState> Pointer;
- MasterState(): serverState(fssBegin), clientReadGreeting(false) {}
+ MasterState(): serverState(fssBegin), clientReadGreeting(false), userDataDone(0), waitForOriginData(false) {}
Ip::Address clientDataAddr; ///< address of our FTP client data connection
SBuf workingDir; ///< estimated current working directory for URI formation
ServerState serverState; ///< what our FTP server is doing
bool clientReadGreeting; ///< whether our FTP client read their FTP server greeting
+ /// Squid will send or has sent this final status code to the FTP client
+ int userDataDone;
+ /// whether the transfer on the Squid-origin data connection is not over yet
+ bool waitForOriginData;
};
/// Manages a control connection from an FTP client.
/* AsyncJob API */
virtual void callException(const std::exception &e);
+ /// Called by Ftp::Client class when it is done receiving or
+ /// sending data. Waits for both agents to be done before
+ /// responding to the FTP client and closing the data connection.
+ void originDataCompletionCheckpoint();
+
// This is a pointer in hope to minimize future changes when MasterState
// becomes a part of MasterXaction. Guaranteed not to be nil.
MasterState::Pointer master; ///< info shared among our FTP client and server jobs
bool createDataConnection(Ip::Address cltAddr);
void closeDataConnection();
+ /// Called after data trasfer on client-to-squid data connection is
+ /// finished.
+ void userDataCompletionCheckpoint(int finalStatusCode);
+
+ /// Writes the data-transfer status reply to the FTP client and
+ /// closes the data connection.
+ void completeDataExchange();
+
void calcUri(const SBuf *file);
void changeState(const Ftp::ServerState newState, const char *reason);
ClientSocketContext *handleUserRequest(const SBuf &cmd, SBuf ¶ms);