From: Dmitry Kurochkin Date: Tue, 16 Apr 2013 13:55:06 +0000 (+0400) Subject: Ftp gateway: initial implementation. X-Git-Tag: SQUID_3_5_0_1~117^2~73 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=434a79b;p=thirdparty%2Fsquid.git Ftp gateway: initial implementation. The first client command must be USER with @[:] parameter. Origin server connection is bound to client connection. After origin server connection is established, it cannot be changed, i.e. client cannot connect to another origin server. Once server or client connection is closed, the other side should be closed as well. Only passive mode is supported on both client and server side. IPv6 (RFC 2428 EPSV command) is not supported. Implemented data transfer commands: RETR, LIST, NLST. Data push commands (STOR, APPE) are not implemented. Many rough edges: aborts/timeouts/error handling, logging and more. --- diff --git a/src/FtpGatewayServer.cc b/src/FtpGatewayServer.cc new file mode 100644 index 0000000000..d74eac1fce --- /dev/null +++ b/src/FtpGatewayServer.cc @@ -0,0 +1,449 @@ +/* + * DEBUG: section 09 File Transfer Protocol (FTP) + * + */ + +#include "squid.h" + +#include "FtpGatewayServer.h" +#include "FtpServer.h" +#include "HttpHdrCc.h" +#include "HttpRequest.h" +#include "Server.h" +#include "SquidTime.h" +#include "Store.h" +#include "client_side.h" +#include "wordlist.h" + +namespace Ftp { + +namespace Gateway { + +class ServerStateData: public Ftp::ServerStateData +{ +public: + ServerStateData(FwdState *const fwdState); + ~ServerStateData(); + + virtual void processReplyBody(); + +protected: + virtual void start(); + + ConnStateData::FtpState clientState() const; + void clientState(ConnStateData::FtpState s) const; + virtual void failed(err_type error = ERR_NONE, int xerrno = 0); + virtual void failedErrorMessage(err_type error, int xerrno); + virtual void handleControlReply(); + virtual void handleRequestBodyProducerAborted(); + virtual bool doneWithServer() const; + void forwardReply(); + void forwardError(err_type error = ERR_NONE, int xerrno = 0); + HttpReply *createHttpReply(const Http::StatusCode httpStatus, const int clen = 0); + void handleDataRequest(); + void startDataTransfer(); + + typedef void (ServerStateData::*PreliminaryCb)(); + void forwardPreliminaryReply(const PreliminaryCb cb); + void proceedAfterPreliminaryReply(); + PreliminaryCb thePreliminaryCb; + + enum { + BEGIN, + SENT_COMMAND, + SENT_PASV, + SENT_DATA_REQUEST, + READING_DATA, + DONE + }; + typedef void (ServerStateData::*SM_FUNC)(); + static const SM_FUNC SM_FUNCS[]; + void readWelcome(); + void sendCommand(); + void readReply(); + void readPasvReply(); + void readDataReply(); + void readTransferDoneReply(); + + virtual void dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t err, int xerrno); + void scheduleReadControlReply(); + + CBDATA_CLASS2(ServerStateData); +}; + +CBDATA_CLASS_INIT(ServerStateData); + +const ServerStateData::SM_FUNC ServerStateData::SM_FUNCS[] = { + &ServerStateData::readWelcome, // BEGIN + &ServerStateData::readReply, // SENT_COMMAND + &ServerStateData::readPasvReply, // SENT_PASV + &ServerStateData::readDataReply, // SENT_DATA_REQUEST + &ServerStateData::readTransferDoneReply, // READING_DATA + NULL // DONE +}; + +ServerStateData::ServerStateData(FwdState *const fwdState): + AsyncJob("Ftp::Gateway::ServerStateData"), Ftp::ServerStateData(fwdState) +{ +} + +ServerStateData::~ServerStateData() +{ + if (Comm::IsConnOpen(ctrl.conn)) { + fwd->unregister(ctrl.conn); + ctrl.forget(); + } +} + +void +ServerStateData::start() +{ + if (clientState() == ConnStateData::FTP_BEGIN) + Ftp::ServerStateData::start(); + else + if (clientState() == ConnStateData::FTP_HANDLE_DATA_REQUEST) + handleDataRequest(); + else + sendCommand(); +} + +ConnStateData::FtpState +ServerStateData::clientState() const +{ + return fwd->request->clientConnectionManager->ftp.state; +} + +void +ServerStateData::clientState(ConnStateData::FtpState s) const +{ + fwd->request->clientConnectionManager->ftp.state = s; +} + +void +ServerStateData::failed(err_type error, int xerrno) +{ + if (!doneWithServer()) + clientState(ConnStateData::FTP_ERROR); + + Ftp::ServerStateData::failed(error, xerrno); +} + +void +ServerStateData::failedErrorMessage(err_type error, int xerrno) +{ + const Http::StatusCode httpStatus = failedHttpStatus(error); + HttpReply *const reply = createHttpReply(httpStatus); + entry->replaceHttpReply(reply); + EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT); + fwd->request->detailError(error, xerrno); +} + +void +ServerStateData::processReplyBody() +{ + debugs(9, 3, HERE << "starting"); + + if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { + /* + * probably was aborted because content length exceeds one + * of the maximum size limits. + */ + abortTransaction("entry aborted after calling appendSuccessHeader()"); + return; + } + +#if USE_ADAPTATION + + if (adaptationAccessCheckPending) { + debugs(9,3, HERE << "returning due to adaptationAccessCheckPending"); + return; + } + +#endif + + if (const int csize = data.readBuf->contentSize()) { + debugs(9, 5, HERE << "writing " << csize << " bytes to the reply"); + addVirginReplyBody(data.readBuf->content(), csize); + data.readBuf->consume(csize); + } + + entry->flush(); + + maybeReadVirginBody(); +} + +void +ServerStateData::handleControlReply() +{ + Ftp::ServerStateData::handleControlReply(); + if (ctrl.message == NULL) + return; // didn't get complete reply yet + + assert(state < DONE); + (this->*SM_FUNCS[state])(); +} + +void +ServerStateData::handleRequestBodyProducerAborted() +{ + ::ServerStateData::handleRequestBodyProducerAborted(); + + abortTransaction("request body producer aborted"); +} + +bool +ServerStateData::doneWithServer() const +{ + return state == DONE || Ftp::ServerStateData::doneWithServer(); +} + +void +ServerStateData::forwardReply() +{ + assert(entry->isEmpty()); + EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT); + + HttpReply *const reply = createHttpReply(Http::scOkay); + + setVirginReply(reply); + adaptOrFinalizeReply(); + + state = DONE; + serverComplete(); +} + +void +ServerStateData::forwardPreliminaryReply(const PreliminaryCb cb) +{ + debugs(9, 5, HERE << "Forwarding preliminary reply to client"); + + assert(thePreliminaryCb == NULL); + thePreliminaryCb = cb; + + const HttpReply::Pointer reply = createHttpReply(Http::scContinue); + + // the Sink will use this to call us back after writing 1xx to the client + typedef NullaryMemFunT CbDialer; + const AsyncCall::Pointer call = JobCallback(11, 3, CbDialer, this, + ServerStateData::proceedAfterPreliminaryReply); + + CallJobHere1(9, 4, request->clientConnectionManager, ConnStateData, + ConnStateData::sendControlMsg, HttpControlMsg(reply, call)); +} + +void +ServerStateData::proceedAfterPreliminaryReply() +{ + debugs(9, 5, HERE << "Proceeding after preliminary reply to client"); + + assert(thePreliminaryCb != NULL); + const PreliminaryCb cb = thePreliminaryCb; + thePreliminaryCb = NULL; + (this->*cb)(); +} + +void +ServerStateData::forwardError(err_type error, int xerrno) +{ + state = DONE; + failed(error, xerrno); +} + +HttpReply * +ServerStateData::createHttpReply(const Http::StatusCode httpStatus, const int clen) +{ + HttpReply *const reply = new HttpReply; + reply->sline.set(Http::ProtocolVersion(1, 1), httpStatus); + HttpHeader &header = reply->header; + header.putTime(HDR_DATE, squid_curtime); + { + HttpHdrCc cc; + cc.Private(); + header.putCc(&cc); + } + if (clen >= 0) + header.putInt64(HDR_CONTENT_LENGTH, clen); + if (ctrl.replycode > 0) + header.putInt(HDR_FTP_STATUS, ctrl.replycode); + if (ctrl.message) { + for (wordlist *W = ctrl.message; W; W = W->next) + header.putStr(HDR_FTP_REASON, W->key); + } else if (ctrl.last_command) + header.putStr(HDR_FTP_REASON, ctrl.last_command); + + reply->hdrCacheInit(); + + return reply; +} + +void +ServerStateData::handleDataRequest() +{ + data.addr = fwd->request->clientConnectionManager->ftp.serverDataAddr; + + if (!data.addr.IsSockAddr()) { // should never happen + debugs(9, DBG_IMPORTANT, HERE << "Inconsistent FTP server state: " + "data.addr=" << data.addr); + failed(); + return; + } + + connectDataChannel(); +} + +void +ServerStateData::startDataTransfer() +{ + assert(Comm::IsConnOpen(data.conn)); + + debugs(9, 3, HERE << "begin data transfer from " << data.conn->remote << + " (" << data.conn->local << ")"); + + HttpReply *const reply = createHttpReply(Http::scOkay, -1); + EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT); + setVirginReply(reply); + adaptOrFinalizeReply(); + + switchTimeoutToDataChannel(); + maybeReadVirginBody(); + state = READING_DATA; +} + +void +ServerStateData::readWelcome() +{ + assert(clientState() == ConnStateData::FTP_BEGIN); + + switch (ctrl.replycode) { + case 220: + clientState(ConnStateData::FTP_CONNECTED); + ctrl.replycode = 120; // change status for forwarded server greeting + forwardPreliminaryReply(&ServerStateData::sendCommand); + break; + case 120: + if (NULL != ctrl.message) + debugs(9, DBG_IMPORTANT, "FTP server is busy: " << ctrl.message->key); + forwardPreliminaryReply(&ServerStateData::scheduleReadControlReply); + break; + default: + failed(); + break; + } +} + +void +ServerStateData::sendCommand() +{ + if (!fwd->request->header.has(HDR_FTP_COMMAND)) { + abortTransaction("Internal error: FTP gateway request with no command"); + return; + } + + String cmd = fwd->request->header.findEntry(HDR_FTP_COMMAND)->value; + const String *const params = fwd->request->header.has(HDR_FTP_ARGUMENTS) ? + &fwd->request->header.findEntry(HDR_FTP_ARGUMENTS)->value : NULL; + + if (params != NULL) + debugs(9, 5, HERE << "command: " << cmd << ", parameters: " << *params); + else + debugs(9, 5, HERE << "command: " << cmd << ", no parameters"); + + static MemBuf mb; + mb.reset(); + if (params != NULL) + mb.Printf("%s %s%s", cmd.termedBuf(), params->termedBuf(), Ftp::crlf); + else + mb.Printf("%s%s", cmd.termedBuf(), Ftp::crlf); + + writeCommand(mb.content()); + + state = clientState() == ConnStateData::FTP_HANDLE_PASV ? SENT_PASV : + clientState() == ConnStateData::FTP_HANDLE_DATA_REQUEST ? SENT_DATA_REQUEST : + SENT_COMMAND; +} + +void +ServerStateData::readReply() +{ + assert(clientState() == ConnStateData::FTP_CONNECTED); + + if (100 <= ctrl.replycode && ctrl.replycode < 200) + forwardPreliminaryReply(&ServerStateData::scheduleReadControlReply); + else + forwardReply(); +} + +void +ServerStateData::readPasvReply() +{ + assert(clientState() == ConnStateData::FTP_HANDLE_PASV); + + if (100 <= ctrl.replycode && ctrl.replycode < 200) + return; // ignore preliminary replies + + if (handlePasvReply()) { + fwd->request->clientConnectionManager->ftp.serverDataAddr = data.addr; + forwardReply(); + } else + forwardError(); +} + +void +ServerStateData::readDataReply() +{ + assert(clientState() == ConnStateData::FTP_HANDLE_DATA_REQUEST); + + if (ctrl.replycode == 150) + forwardPreliminaryReply(&ServerStateData::startDataTransfer); + else + forwardReply(); +} + +void +ServerStateData::readTransferDoneReply() +{ + debugs(9, 3, HERE); + + if (ctrl.replycode != 226 && ctrl.replycode != 250) { + debugs(9, DBG_IMPORTANT, HERE << "Got code " << ctrl.replycode << + " after reading data"); + } + + state = DONE; + serverComplete(); +} + +void +ServerStateData::dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t err, int xerrno) +{ + debugs(9, 3, HERE); + data.opener = NULL; + + if (err != COMM_OK) { + debugs(9, 2, HERE << "Failed to connect FTP server data channel."); + forwardError(ERR_CONNECT_FAIL, xerrno); + return; + } + + debugs(9, 2, HERE << "Connected FTP server data channel: " << conn); + + data.opened(conn, dataCloser()); + + sendCommand(); +} + +void +ServerStateData::scheduleReadControlReply() +{ + Ftp::ServerStateData::scheduleReadControlReply(0); +} + +}; // namespace Gateway + +}; // namespace Ftp + +void +ftpGatewayServerStart(FwdState *const fwdState) +{ + AsyncJob::Start(new Ftp::Gateway::ServerStateData(fwdState)); +} diff --git a/src/FtpGatewayServer.h b/src/FtpGatewayServer.h new file mode 100644 index 0000000000..bbfe99d4ae --- /dev/null +++ b/src/FtpGatewayServer.h @@ -0,0 +1,13 @@ +/* + * DEBUG: section 09 File Transfer Protocol (FTP) + * + */ + +#ifndef SQUID_FTP_GATEWAY_SERVER_H +#define SQUID_FTP_GATEWAY_SERVER_H + +class FwdState; + +void ftpGatewayServerStart(FwdState *const); + +#endif /* SQUID_FTP_GATEWAY_SERVER_H */ diff --git a/src/FtpServer.cc b/src/FtpServer.cc new file mode 100644 index 0000000000..bb51afbfc1 --- /dev/null +++ b/src/FtpServer.cc @@ -0,0 +1,848 @@ +/* + * DEBUG: section 09 File Transfer Protocol (FTP) + * + */ + +#include "squid.h" + +#include "FtpServer.h" +#include "Mem.h" +#include "SquidConfig.h" +#include "StatCounters.h" +#include "client_side.h" +#include "comm/ConnOpener.h" +#include "comm/Write.h" +#include "errorpage.h" +#include "fd.h" +#include "tools.h" +#include "wordlist.h" + +namespace Ftp { + +const char *const crlf = "\r\n"; + +/// \ingroup ServerProtocolFTPInternal +static char * +escapeIAC(const char *buf) +{ + int n; + char *ret; + unsigned const char *p; + unsigned char *r; + + for (p = (unsigned const char *)buf, n = 1; *p; ++n, ++p) + if (*p == 255) + ++n; + + ret = (char *)xmalloc(n); + + for (p = (unsigned const char *)buf, r=(unsigned char *)ret; *p; ++p) { + *r = *p; + ++r; + + if (*p == 255) { + *r = 255; + ++r; + } + } + + *r = '\0'; + ++r; + assert((r - (unsigned char *)ret) == n ); + return ret; +} + +/// configures the channel with a descriptor and registers a close handler +void +ServerChannel::opened(const Comm::ConnectionPointer &newConn, + const AsyncCall::Pointer &aCloser) +{ + assert(!Comm::IsConnOpen(conn)); + assert(closer == NULL); + + assert(Comm::IsConnOpen(newConn)); + assert(aCloser != NULL); + + conn = newConn; + closer = aCloser; + comm_add_close_handler(conn->fd, closer); +} + +/// planned close: removes the close handler and calls comm_close +void +ServerChannel::close() +{ + // channels with active listeners will be closed when the listener handler dies. + if (Comm::IsConnOpen(conn)) { + comm_remove_close_handler(conn->fd, closer); + conn->close(); // we do not expect to be called back + } + clear(); +} + +void +ServerChannel::forget() +{ + if (Comm::IsConnOpen(conn)) + comm_remove_close_handler(conn->fd, closer); + closer = NULL; +} + +void +ServerChannel::clear() +{ + conn = NULL; + closer = NULL; +} + +ServerStateData::ServerStateData(FwdState *fwdState): + AsyncJob("Ftp::ServerStateData"), ::ServerStateData(fwdState) +{ + ++statCounter.server.all.requests; + ++statCounter.server.ftp.requests; + + ctrl.last_command = xstrdup("Connect to server"); + ctrl.buf = static_cast(memAllocBuf(4096, &ctrl.size)); + ctrl.offset = 0; + + typedef CommCbMemFunT Dialer; + const AsyncCall::Pointer closer = JobCallback(9, 5, Dialer, this, + ServerStateData::ctrlClosed); + ctrl.opened(fwdState->serverConnection(), closer); +} + +ServerStateData::~ServerStateData() +{ + if (data.opener != NULL) { + data.opener->cancel("Ftp::ServerStateData destructed"); + data.opener = NULL; + } + data.close(); + + if (ctrl.buf) { + memFreeBuf(ctrl.size, ctrl.buf); + ctrl.buf = NULL; + } + if (ctrl.message) + wordlistDestroy(&ctrl.message); + safe_free(ctrl.last_command); + safe_free(ctrl.last_reply); + + if (data.readBuf) { + if (!data.readBuf->isNull()) + data.readBuf->clean(); + + delete data.readBuf; + } + + safe_free(old_request); + + safe_free(old_reply); + + fwd = NULL; // refcounted +} + +void +ServerStateData::start() +{ + scheduleReadControlReply(0); +} + +/** + * Close the FTP server connection(s). Used by serverComplete(). + */ +void +ServerStateData::closeServer() +{ + if (Comm::IsConnOpen(ctrl.conn)) { + debugs(9,3, HERE << "closing FTP server FD " << ctrl.conn->fd << ", this " << this); + fwd->unregister(ctrl.conn); + ctrl.close(); + } + + if (Comm::IsConnOpen(data.conn)) { + debugs(9,3, HERE << "closing FTP data FD " << data.conn->fd << ", this " << this); + data.close(); + } + + debugs(9,3, HERE << "FTP ctrl and data connections closed. this " << this); +} + +/** + * Did we close all FTP server connection(s)? + * + \retval true Both server control and data channels are closed. And not waiting for a new data connection to open. + \retval false Either control channel or data is still active. + */ +bool +ServerStateData::doneWithServer() const +{ + return !Comm::IsConnOpen(ctrl.conn) && !Comm::IsConnOpen(data.conn); +} + +void +ServerStateData::failed(err_type error, int xerrno) +{ + debugs(9,3,HERE << "entry-null=" << (entry?entry->isEmpty():0) << ", entry=" << entry); + if (entry->isEmpty()) + failedErrorMessage(error, xerrno); + + serverComplete(); +} + +void +ServerStateData::failedErrorMessage(err_type error, int xerrno) +{ + const char *command, *reply; + const Http::StatusCode httpStatus = failedHttpStatus(error); + ErrorState *const ftperr = new ErrorState(error, httpStatus, fwd->request); + ftperr->xerrno = xerrno; + + ftperr->ftp.server_msg = ctrl.message; + ctrl.message = NULL; + + if (old_request) + command = old_request; + else + command = ctrl.last_command; + + if (command && strncmp(command, "PASS", 4) == 0) + command = "PASS "; + + if (old_reply) + reply = old_reply; + else + reply = ctrl.last_reply; + + if (command) + ftperr->ftp.request = xstrdup(command); + + if (reply) + ftperr->ftp.reply = xstrdup(reply); + + entry->replaceHttpReply( ftperr->BuildHttpReply() ); + delete ftperr; +} + +Http::StatusCode +ServerStateData::failedHttpStatus(err_type &error) +{ + if (error == ERR_NONE) + error = ERR_FTP_FAILURE; + return error == ERR_READ_TIMEOUT ? Http::scGateway_Timeout : + Http::scBadGateway; +} + +/** + * DPW 2007-04-23 + * Looks like there are no longer anymore callers that set + * buffered_ok=1. Perhaps it can be removed at some point. + */ +void +ServerStateData::scheduleReadControlReply(int buffered_ok) +{ + debugs(9, 3, HERE << ctrl.conn); + + if (buffered_ok && ctrl.offset > 0) { + /* We've already read some reply data */ + handleControlReply(); + } else { + /* + * Cancel the timeout on the Data socket (if any) and + * establish one on the control socket. + */ + if (Comm::IsConnOpen(data.conn)) { + commUnsetConnTimeout(data.conn); + } + + typedef CommCbMemFunT TimeoutDialer; + AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, ServerStateData::timeout); + commSetConnTimeout(ctrl.conn, Config.Timeout.read, timeoutCall); + + typedef CommCbMemFunT Dialer; + AsyncCall::Pointer reader = JobCallback(9, 5, Dialer, this, ServerStateData::readControlReply); + comm_read(ctrl.conn, ctrl.buf + ctrl.offset, ctrl.size - ctrl.offset, reader); + } +} + +void +ServerStateData::readControlReply(const CommIoCbParams &io) +{ + debugs(9, 3, HERE << "FD " << io.fd << ", Read " << io.size << " bytes"); + + if (io.size > 0) { + kb_incr(&(statCounter.server.all.kbytes_in), io.size); + kb_incr(&(statCounter.server.ftp.kbytes_in), io.size); + } + + if (io.flag == COMM_ERR_CLOSING) + return; + + if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { + abortTransaction("entry aborted during control reply read"); + return; + } + + assert(ctrl.offset < ctrl.size); + + if (io.flag == COMM_OK && io.size > 0) { + fd_bytes(io.fd, io.size, FD_READ); + } + + if (io.flag != COMM_OK) { + debugs(50, ignoreErrno(io.xerrno) ? 3 : DBG_IMPORTANT, + "ftpReadControlReply: read error: " << xstrerr(io.xerrno)); + + if (ignoreErrno(io.xerrno)) { + scheduleReadControlReply(0); + } else { + failed(ERR_READ_ERROR, io.xerrno); + /* failed closes ctrl.conn and frees ftpState */ + } + return; + } + + if (io.size == 0) { + if (entry->store_status == STORE_PENDING) { + failed(ERR_FTP_FAILURE, 0); + /* failed closes ctrl.conn and frees ftpState */ + return; + } + + /* XXX this may end up having to be serverComplete() .. */ + abortTransaction("zero control reply read"); + return; + } + + unsigned int len =io.size + ctrl.offset; + ctrl.offset = len; + assert(len <= ctrl.size); + handleControlReply(); +} + +void +ServerStateData::handleControlReply() +{ + debugs(9, 3, HERE); + + size_t bytes_used = 0; + wordlistDestroy(&ctrl.message); + ctrl.message = parseControlReply(ctrl.buf, ctrl.offset, &ctrl.replycode, + &bytes_used); + + if (ctrl.message == NULL) { + /* didn't get complete reply yet */ + + if (ctrl.offset == ctrl.size) { + ctrl.buf = (char *)memReallocBuf(ctrl.buf, ctrl.size << 1, &ctrl.size); + } + + scheduleReadControlReply(0); + return; + } else if (ctrl.offset == bytes_used) { + /* used it all up */ + ctrl.offset = 0; + } else { + /* Got some data past the complete reply */ + assert(bytes_used < ctrl.offset); + ctrl.offset -= bytes_used; + memmove(ctrl.buf, ctrl.buf + bytes_used, ctrl.offset); + } + + /* Move the last line of the reply message to ctrl.last_reply */ + const wordlist *W; + for (W = ctrl.message; W && W->next; W = W->next); + if (W) { + safe_free(ctrl.last_reply); + ctrl.last_reply = xstrdup(W->key); + } + + debugs(9, 3, HERE << "state=" << state << ", code=" << ctrl.replycode); +} + +bool +ServerStateData::handlePasvReply() +{ + int code = ctrl.replycode; + int h1, h2, h3, h4; + int p1, p2; + int n; + unsigned short port; + Ip::Address ipa_remote; + char *buf; + LOCAL_ARRAY(char, ipaddr, 1024); + debugs(9, 3, HERE); + + if (code != 227) { + debugs(9, 2, "PASV not supported by remote end"); + return false; + } + + /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */ + /* ANSI sez [^0-9] is undefined, it breaks on Watcom cc */ + debugs(9, 5, HERE << "scanning: " << ctrl.last_reply); + + buf = ctrl.last_reply + strcspn(ctrl.last_reply, "0123456789"); + + n = sscanf(buf, "%d,%d,%d,%d,%d,%d", &h1, &h2, &h3, &h4, &p1, &p2); + + if (n != 6 || p1 < 0 || p2 < 0 || p1 > 255 || p2 > 255) { + debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << + ctrl.conn->remote << ": " << ctrl.last_reply); + return false; + } + + snprintf(ipaddr, 1024, "%d.%d.%d.%d", h1, h2, h3, h4); + + ipa_remote = ipaddr; + + if ( ipa_remote.IsAnyAddr() ) { + debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << + ctrl.conn->remote << ": " << ctrl.last_reply); + return false; + } + + port = ((p1 << 8) + p2); + + if (0 == port) { + debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << + ctrl.conn->remote << ": " << ctrl.last_reply); + return false; + } + + if (Config.Ftp.sanitycheck) { + if (port < 1024) { + debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << + ctrl.conn->remote << ": " << ctrl.last_reply); + return false; + } + } + + data.addr = Config.Ftp.sanitycheck ? fd_table[ctrl.conn->fd].ipaddr : ipaddr; + data.addr.SetPort(port); + + return true; +} + +void +ServerStateData::connectDataChannel() +{ + safe_free(ctrl.last_command); + + safe_free(ctrl.last_reply); + + ctrl.last_command = xstrdup("Connect to server data port"); + + Comm::ConnectionPointer conn = new Comm::Connection; + conn->local = ctrl.conn->local; + conn->local.SetPort(0); + conn->remote = data.addr; + + debugs(9, 3, HERE << "connecting to " << conn->remote); + + data.opener = commCbCall(9,3, "Ftp::ServerStateData::dataChannelConnected", + CommConnectCbPtrFun(ServerStateData::dataChannelConnected, this)); + Comm::ConnOpener *cs = new Comm::ConnOpener(conn, data.opener, Config.Timeout.connect); + char buf[MAX_IPSTRLEN]; + data.addr.ToHostname(buf, MAX_IPSTRLEN); + cs->setHost(buf); + AsyncJob::Start(cs); +} + +void +ServerStateData::dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno, void *data) +{ + ServerStateData *ftpState = static_cast(data); + ftpState->dataChannelConnected(conn, status, xerrno); +} + +/// creates a data channel Comm close callback +AsyncCall::Pointer +ServerStateData::dataCloser() +{ + typedef CommCbMemFunT Dialer; + return JobCallback(9, 5, Dialer, this, ServerStateData::dataClosed); +} + +/// handler called by Comm when FTP data channel is closed unexpectedly +void +ServerStateData::dataClosed(const CommCloseCbParams &io) +{ + debugs(9, 4, HERE); + if (data.listenConn != NULL) { + data.listenConn->close(); + data.listenConn = NULL; + // NP clear() does the: data.fd = -1; + } + data.clear(); +} + +void +ServerStateData::writeCommand(const char *buf) +{ + char *ebuf; + /* trace FTP protocol communications at level 2 */ + debugs(9, 2, "ftp<< " << buf); + + if (Config.Ftp.telnet) + ebuf = escapeIAC(buf); + else + ebuf = xstrdup(buf); + + safe_free(ctrl.last_command); + + safe_free(ctrl.last_reply); + + ctrl.last_command = ebuf; + + if (!Comm::IsConnOpen(ctrl.conn)) { + debugs(9, 2, HERE << "cannot send to closing ctrl " << ctrl.conn); + // TODO: assert(ctrl.closer != NULL); + return; + } + + typedef CommCbMemFunT Dialer; + AsyncCall::Pointer call = JobCallback(9, 5, Dialer, this, + ServerStateData::writeCommandCallback); + Comm::Write(ctrl.conn, ctrl.last_command, strlen(ctrl.last_command), call, NULL); + + scheduleReadControlReply(0); +} + +void +ServerStateData::writeCommandCallback(const CommIoCbParams &io) +{ + + debugs(9, 5, HERE << "wrote " << io.size << " bytes"); + + if (io.size > 0) { + fd_bytes(io.fd, io.size, FD_WRITE); + kb_incr(&(statCounter.server.all.kbytes_out), io.size); + kb_incr(&(statCounter.server.ftp.kbytes_out), io.size); + } + + if (io.flag == COMM_ERR_CLOSING) + return; + + if (io.flag) { + debugs(9, DBG_IMPORTANT, "ftpWriteCommandCallback: " << io.conn << ": " << xstrerr(io.xerrno)); + failed(ERR_WRITE_ERROR, io.xerrno); + /* failed closes ctrl.conn and frees ftpState */ + return; + } +} + +/// handler called by Comm when FTP control channel is closed unexpectedly +void +ServerStateData::ctrlClosed(const CommCloseCbParams &io) +{ + debugs(9, 4, HERE); + ctrl.clear(); + mustStop("Ftp::ServerStateData::ctrlClosed"); +} + +void +ServerStateData::timeout(const CommTimeoutCbParams &io) +{ + debugs(9, 4, HERE << io.conn << ": '" << entry->url() << "'" ); + + if (abortOnBadEntry("entry went bad while waiting for a timeout")) + return; + + failed(ERR_READ_TIMEOUT, 0); + /* failed() closes ctrl.conn and frees ftpState */ +} + +const Comm::ConnectionPointer & +ServerStateData::dataConnection() const +{ + return data.conn; +} + +void +ServerStateData::maybeReadVirginBody() +{ + // too late to read + if (!Comm::IsConnOpen(data.conn) || fd_table[data.conn->fd].closing()) + return; + + if (data.read_pending) + return; + + if (data.readBuf == NULL) { + data.readBuf = new MemBuf; + data.readBuf->init(4096, SQUID_TCP_SO_RCVBUF); + } + const int read_sz = replyBodySpace(*data.readBuf, 0); + + debugs(11,9, HERE << "FTP may read up to " << read_sz << " bytes"); + + if (read_sz < 2) // see http.cc + return; + + data.read_pending = true; + + typedef CommCbMemFunT TimeoutDialer; + AsyncCall::Pointer timeoutCall = JobCallback(9, 5, + TimeoutDialer, this, ServerStateData::timeout); + commSetConnTimeout(data.conn, Config.Timeout.read, timeoutCall); + + debugs(9,5,HERE << "queueing read on FD " << data.conn->fd); + + typedef CommCbMemFunT Dialer; + entry->delayAwareRead(data.conn, data.readBuf->space(), read_sz, + JobCallback(9, 5, Dialer, this, ServerStateData::dataRead)); +} + +void +ServerStateData::dataRead(const CommIoCbParams &io) +{ + int j; + int bin; + + data.read_pending = false; + + debugs(9, 3, HERE << "FD " << io.fd << " Read " << io.size << " bytes"); + + if (io.size > 0) { + kb_incr(&(statCounter.server.all.kbytes_in), io.size); + kb_incr(&(statCounter.server.ftp.kbytes_in), io.size); + } + + if (io.flag == COMM_ERR_CLOSING) + return; + + assert(io.fd == data.conn->fd); + + if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { + abortTransaction("entry aborted during dataRead"); + return; + } + + if (io.flag == COMM_OK && io.size > 0) { + debugs(9, 5, HERE << "appended " << io.size << " bytes to readBuf"); + data.readBuf->appended(io.size); +#if USE_DELAY_POOLS + DelayId delayId = entry->mem_obj->mostBytesAllowed(); + delayId.bytesIn(io.size); +#endif + ++ IOStats.Ftp.reads; + + for (j = io.size - 1, bin = 0; j; ++bin) + j >>= 1; + + ++ IOStats.Ftp.read_hist[bin]; + } + + if (io.flag != COMM_OK) { + debugs(50, ignoreErrno(io.xerrno) ? 3 : DBG_IMPORTANT, + HERE << "read error: " << xstrerr(io.xerrno)); + + if (ignoreErrno(io.xerrno)) { + typedef CommCbMemFunT TimeoutDialer; + AsyncCall::Pointer timeoutCall = + JobCallback(9, 5, TimeoutDialer, this, + ServerStateData::timeout); + commSetConnTimeout(io.conn, Config.Timeout.read, timeoutCall); + + maybeReadVirginBody(); + } else { + failed(ERR_READ_ERROR, 0); + /* failed closes ctrl.conn and frees ftpState */ + return; + } + } else if (io.size == 0) { + debugs(9,3, HERE << "Calling dataComplete() because io.size == 0"); + /* + * DPW 2007-04-23 + * Dangerous curves ahead. This call to dataComplete was + * calling scheduleReadControlReply, handleControlReply, + * and then ftpReadTransferDone. If ftpReadTransferDone + * gets unexpected status code, it closes down the control + * socket and our FtpStateData object gets destroyed. As + * a workaround we no longer set the 'buffered_ok' flag in + * the scheduleReadControlReply call. + */ + dataComplete(); + } + + processReplyBody(); +} + +void +ServerStateData::dataComplete() +{ + debugs(9, 3,HERE); + + /* Connection closed; transfer done. */ + + /// Close data channel, if any, to conserve resources while we wait. + data.close(); + + /* expect the "transfer complete" message on the control socket */ + /* + * DPW 2007-04-23 + * Previously, this was the only place where we set the + * 'buffered_ok' flag when calling scheduleReadControlReply(). + * It caused some problems if the FTP server returns an unexpected + * status code after the data command. FtpStateData was being + * deleted in the middle of dataRead(). + */ + /* AYJ: 2011-01-13: Bug 2581. + * 226 status is possibly waiting in the ctrl buffer. + * The connection will hang if we DONT send buffered_ok. + * This happens on all transfers which can be completly sent by the + * server before the 150 started status message is read in by Squid. + * ie all transfers of about one packet hang. + */ + scheduleReadControlReply(1); +} + +/** + * Quickly abort the transaction + * + \todo destruction should be sufficient as the destructor should cleanup, + * including canceling close handlers + */ +void +ServerStateData::abortTransaction(const char *reason) +{ + debugs(9, 3, HERE << "aborting transaction for " << reason << + "; FD " << (ctrl.conn!=NULL?ctrl.conn->fd:-1) << ", Data FD " << (data.conn!=NULL?data.conn->fd:-1) << ", this " << this); + if (Comm::IsConnOpen(ctrl.conn)) { + ctrl.conn->close(); + return; + } + + fwd->handleUnregisteredServerEnd(); + mustStop("ServerStateData::abortTransaction"); +} + +/** + * Cancel the timeout on the Control socket and establish one + * on the data socket + */ +void +ServerStateData::switchTimeoutToDataChannel() +{ + commUnsetConnTimeout(ctrl.conn); + + typedef CommCbMemFunT TimeoutDialer; + AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, + ServerStateData::timeout); + commSetConnTimeout(data.conn, Config.Timeout.read, timeoutCall); +} + +void +ServerStateData::sentRequestBody(const CommIoCbParams &io) +{ + if (io.size > 0) + kb_incr(&(statCounter.server.ftp.kbytes_out), io.size); + ::ServerStateData::sentRequestBody(io); +} + +/** + * called after we wrote the last byte of the request body + */ +void +ServerStateData::doneSendingRequestBody() +{ + ::ServerStateData::doneSendingRequestBody(); + debugs(9,3, HERE); + dataComplete(); + /* NP: RFC 959 3.3. DATA CONNECTION MANAGEMENT + * if transfer type is 'stream' call dataComplete() + * otherwise leave open. (reschedule control channel read?) + */ +} + +wordlist * +ServerStateData::parseControlReply(char *buf, size_t len, int *codep, size_t *used) +{ + char *s; + char *sbuf; + char *end; + int usable; + int complete = 0; + wordlist *head = NULL; + wordlist *list; + wordlist **tail = &head; + size_t offset; + size_t linelen; + int code = -1; + debugs(9, 3, HERE); + /* + * We need a NULL-terminated buffer for scanning, ick + */ + sbuf = (char *)xmalloc(len + 1); + xstrncpy(sbuf, buf, len + 1); + end = sbuf + len - 1; + + while (*end != '\r' && *end != '\n' && end > sbuf) + --end; + + usable = end - sbuf; + + debugs(9, 3, HERE << "usable = " << usable); + + if (usable == 0) { + debugs(9, 3, HERE << "didn't find end of line"); + safe_free(sbuf); + return NULL; + } + + debugs(9, 3, HERE << len << " bytes to play with"); + ++end; + s = sbuf; + s += strspn(s, crlf); + + for (; s < end; s += strcspn(s, crlf), s += strspn(s, crlf)) { + if (complete) + break; + + debugs(9, 5, HERE << "s = {" << s << "}"); + + linelen = strcspn(s, crlf) + 1; + + if (linelen < 2) + break; + + if (linelen > 3) + complete = (*s >= '0' && *s <= '9' && *(s + 3) == ' '); + + if (complete) + code = atoi(s); + + offset = 0; + + if (linelen > 3) + if (*s >= '0' && *s <= '9' && (*(s + 3) == '-' || *(s + 3) == ' ')) + offset = 4; + + list = new wordlist(); + + list->key = (char *)xmalloc(linelen - offset); + + xstrncpy(list->key, s + offset, linelen - offset); + + /* trace the FTP communication chat at level 2 */ + debugs(9, 2, "ftp>> " << code << " " << list->key); + + *tail = list; + + tail = &list->next; + } + + *used = (size_t) (s - sbuf); + safe_free(sbuf); + + if (!complete) + wordlistDestroy(&head); + + if (codep) + *codep = code; + + return head; +} + +}; // namespace Ftp diff --git a/src/FtpServer.h b/src/FtpServer.h new file mode 100644 index 0000000000..5e7f02d70e --- /dev/null +++ b/src/FtpServer.h @@ -0,0 +1,117 @@ +/* + * DEBUG: section 09 File Transfer Protocol (FTP) + * + */ + +#ifndef SQUID_FTP_SERVER_H +#define SQUID_FTP_SERVER_H + +#include "Server.h" + +namespace Ftp { + +extern const char *const crlf; + +/// common code for FTP server control and data channels +/// does not own the channel descriptor, which is managed by FtpStateData +class ServerChannel +{ +public: + /// called after the socket is opened, sets up close handler + void opened(const Comm::ConnectionPointer &conn, const AsyncCall::Pointer &aCloser); + + /** Handles all operations needed to properly close the active channel FD. + * clearing the close handler, clearing the listen socket properly, and calling comm_close + */ + void close(); + + void forget(); /// remove the close handler, leave connection open + + void clear(); ///< just drops conn and close handler. does not close active connections. + + Comm::ConnectionPointer conn; ///< channel descriptor + + /** A temporary handle to the connection being listened on. + * Closing this will also close the waiting Data channel acceptor. + * If a data connection has already been accepted but is still waiting in the event queue + * the callback will still happen and needs to be handled (usually dropped). + */ + Comm::ConnectionPointer listenConn; + + AsyncCall::Pointer opener; ///< Comm opener handler callback. +private: + AsyncCall::Pointer closer; ///< Comm close handler callback +}; + +/// Base class for FTP over HTTP and FTP Gateway server state. +class ServerStateData: public ::ServerStateData +{ +public: + ServerStateData(FwdState *fwdState); + virtual ~ServerStateData(); + + virtual void failed(err_type error = ERR_NONE, int xerrno = 0); + virtual void timeout(const CommTimeoutCbParams &io); + virtual const Comm::ConnectionPointer & dataConnection() const; + virtual void abortTransaction(const char *reason); + void writeCommand(const char *buf); + bool handlePasvReply(); + void connectDataChannel(); + virtual void maybeReadVirginBody(); + void switchTimeoutToDataChannel(); + + // \todo: optimize ctrl and data structs member order, to minimize size + /// FTP control channel info; the channel is opened once per transaction + struct CtrlChannel: public ServerChannel { + char *buf; + size_t size; + size_t offset; + wordlist *message; + char *last_command; + char *last_reply; + int replycode; + } ctrl; + + /// FTP data channel info; the channel may be opened/closed a few times + struct DataChannel: public ServerChannel { + MemBuf *readBuf; + Ip::Address addr; + bool read_pending; + } data; + + int state; + char *old_request; + char *old_reply; + +protected: + virtual void start(); + + virtual void closeServer(); + virtual bool doneWithServer() const; + virtual void failedErrorMessage(err_type error, int xerrno); + virtual Http::StatusCode failedHttpStatus(err_type &error); + void ctrlClosed(const CommCloseCbParams &io); + void scheduleReadControlReply(int buffered_ok); + void readControlReply(const CommIoCbParams &io); + virtual void handleControlReply(); + void writeCommandCallback(const CommIoCbParams &io); + static CNCB dataChannelConnected; + virtual void dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno) = 0; + void dataRead(const CommIoCbParams &io); + void dataComplete(); + AsyncCall::Pointer dataCloser(); + virtual void dataClosed(const CommCloseCbParams &io); + + // sending of the request body to the server + virtual void sentRequestBody(const CommIoCbParams &io); + virtual void doneSendingRequestBody(); + +private: + static wordlist *parseControlReply(char *buf, size_t len, int *codep, size_t *used); + + CBDATA_CLASS2(ServerStateData); +}; + +}; // namespace Ftp + +#endif /* SQUID_FTP_SERVER_H */ diff --git a/src/HttpHdrCc.h b/src/HttpHdrCc.h index af1cb4a656..1781c83d19 100644 --- a/src/HttpHdrCc.h +++ b/src/HttpHdrCc.h @@ -75,7 +75,7 @@ public: //manipulation for Cache-Control: private header bool hasPrivate() const {return isSet(CC_PRIVATE);} const String &Private() const {return private_;} - void Private(String &v) { + void Private(const String &v = "") { setMask(CC_PRIVATE,true); // uses append for multi-line headers if (private_.defined()) diff --git a/src/HttpHeader.cc b/src/HttpHeader.cc index fdfa8ac0c9..ee08bea38b 100644 --- a/src/HttpHeader.cc +++ b/src/HttpHeader.cc @@ -161,6 +161,10 @@ static const HttpHeaderFieldAttrs HeadersAttrs[] = { {"Surrogate-Capability", HDR_SURROGATE_CAPABILITY, ftStr}, {"Surrogate-Control", HDR_SURROGATE_CONTROL, ftPSc}, {"Front-End-Https", HDR_FRONT_END_HTTPS, ftStr}, + {"FTP-Command", HDR_FTP_COMMAND, ftStr}, + {"FTP-Arguments", HDR_FTP_ARGUMENTS, ftStr}, + {"FTP-Status", HDR_FTP_STATUS, ftInt}, + {"FTP-Reason", HDR_FTP_REASON, ftStr}, {"Other:", HDR_OTHER, ftStr} /* ':' will not allow matches */ }; diff --git a/src/HttpHeader.h b/src/HttpHeader.h index f595b96f63..1013d528b6 100644 --- a/src/HttpHeader.h +++ b/src/HttpHeader.h @@ -146,6 +146,10 @@ typedef enum { HDR_SURROGATE_CAPABILITY, /**< Edge Side Includes (ESI) header */ HDR_SURROGATE_CONTROL, /**< Edge Side Includes (ESI) header */ HDR_FRONT_END_HTTPS, /**< MS Exchange custom header we may have to add */ + HDR_FTP_COMMAND, /**< Internal header for FTP command */ + HDR_FTP_ARGUMENTS, /**< Internal header for FTP command arguments */ + HDR_FTP_STATUS, /**< Internal header for FTP reply status */ + HDR_FTP_REASON, /**< Internal header for FTP reply reason */ HDR_OTHER, /**< internal tag value for "unknown" headers */ HDR_ENUM_END } http_hdr_type; diff --git a/src/HttpReply.h b/src/HttpReply.h index c7f3f3e364..1908e97280 100644 --- a/src/HttpReply.h +++ b/src/HttpReply.h @@ -131,6 +131,8 @@ public: /// Remove Warnings with warn-date different from Date value void removeStaleWarnings(); + virtual void hdrCacheInit(); + private: /** initialize */ void init(); @@ -161,8 +163,6 @@ protected: virtual void packFirstLineInto(Packer * p, bool) const { sline.packInto(p); } virtual bool parseFirstLine(const char *start, const char *end); - - virtual void hdrCacheInit(); }; MEMPROXY_CLASS_INLINE(HttpReply); diff --git a/src/Makefile.am b/src/Makefile.am index 8cab7437f9..24c59f762f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -355,6 +355,10 @@ squid_SOURCES = \ fqdncache.cc \ ftp.h \ ftp.cc \ + FtpServer.h \ + FtpServer.cc \ + FtpGatewayServer.h \ + FtpGatewayServer.cc \ Generic.h \ globals.h \ gopher.h \ diff --git a/src/SquidConfig.h b/src/SquidConfig.h index b64ae89356..5f4fab694d 100644 --- a/src/SquidConfig.h +++ b/src/SquidConfig.h @@ -145,6 +145,7 @@ public: #if USE_SSL AnyP::PortCfg *https; #endif + AnyP::PortCfg *ftp; } Sockaddr; #if SQUID_SNMP diff --git a/src/cache_cf.cc b/src/cache_cf.cc index 9485c8eca2..4fff598396 100644 --- a/src/cache_cf.cc +++ b/src/cache_cf.cc @@ -3791,6 +3791,8 @@ parsePortCfg(AnyP::PortCfg ** head, const char *optionName) protocol = "http"; else if (strcmp(optionName, "https_port") == 0) protocol = "https"; + else if (strcmp(optionName, "ftp_port") == 0) + protocol = "ftp"; if (!protocol) { self_destruct(); return; @@ -3823,6 +3825,12 @@ parsePortCfg(AnyP::PortCfg ** head, const char *optionName) debugs(3, DBG_CRITICAL, "FATAL: tproxy/intercept on https_port requires ssl-bump which is missing."); self_destruct(); } + } else if (strcmp(protocol, "ftp") == 0) { + /* ftp_port does not support ssl-bump */ + if (s->flags.tunnelSslBumping) { + debugs(3, DBG_CRITICAL, "FATAL: ssl-bump is not supported for ftp_port."); + self_destruct(); + } } #endif diff --git a/src/cf.data.pre b/src/cf.data.pre index e38603b1b5..dbd6217e23 100644 --- a/src/cf.data.pre +++ b/src/cf.data.pre @@ -1848,6 +1848,13 @@ DOC_START See http_port for a list of available options. DOC_END +NAME: ftp_port +TYPE: PortCfg +DEFAULT: none +LOC: Config.Sockaddr.ftp +DOC_START + Usage: [ip:]port + NAME: tcp_outgoing_tos tcp_outgoing_ds tcp_outgoing_dscp TYPE: acl_tos DEFAULT: none diff --git a/src/client_side.cc b/src/client_side.cc index 626018be25..5067d9226d 100644 --- a/src/client_side.cc +++ b/src/client_side.cc @@ -219,6 +219,7 @@ static IOACB httpAccept; #if USE_SSL static IOACB httpsAccept; #endif +static IOACB ftpAccept; static CTCB clientLifetimeTimeout; static ClientSocketContext *parseHttpRequestAbort(ConnStateData * conn, const char *uri); static ClientSocketContext *parseHttpRequest(ConnStateData *, HttpParser *, HttpRequestMethod *, Http::ProtocolVersion *); @@ -248,6 +249,37 @@ static void connNoteUseOfBuffer(ConnStateData* conn, size_t byteCount); static ConnStateData *connStateCreate(const Comm::ConnectionPointer &client, AnyP::PortCfg *port); +static IOACB FtpAcceptDataConnection; +static void FtpCloseDataConnection(ConnStateData *conn); +static void FtpWriteGreeting(ConnStateData *conn); +static ClientSocketContext *FtpParseRequest(ConnStateData *connState, HttpRequestMethod *method_p, Http::ProtocolVersion *http_ver); + +static void FtpHandleReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data); +typedef void FtpReplyHandler(ClientSocketContext *context, const HttpReply *reply); +static FtpReplyHandler FtpHandlePasvReply; +static FtpReplyHandler FtpHandleErrorReply; + +static void FtpHandleReplyData(ClientSocketContext *context, StoreIOBuffer data); +static IOCB FtpWroteReplyData; + +static void FtpWriteEarlyReply(ConnStateData *conn, const int code, const char *msg); +static void FtpWriteReply(ClientSocketContext *context, MemBuf &mb); +static void FtpWriteCustomReply(ClientSocketContext *context, const int code, const char *msg, const HttpReply *reply = NULL); +static void FtpWriteForwardedReply(ClientSocketContext *context, const HttpReply *reply); +static void FtpWriteForwardedReply(ClientSocketContext *context, const HttpReply *reply, AsyncCall::Pointer call); +static void FtpPrintReply(MemBuf &mb, const HttpReply *reply, const char *const prefix = ""); +static IOCB FtpWroteEarlyReply; +static IOCB FtpWroteReply; + +typedef bool FtpRequestHandler(ConnStateData *connState, String &cmd, String ¶ms); +static FtpRequestHandler FtpHandleRequest; +static FtpRequestHandler FtpHandleUserRequest; +static FtpRequestHandler FtpHandlePasvRequest; +static FtpRequestHandler FtpHandlePortRequest; +static FtpRequestHandler FtpHandleDataRequest; + +static bool FtpCheckDataConnection(ConnStateData *connState); + clientStreamNode * ClientSocketContext::getTail() const { @@ -263,6 +295,12 @@ ClientSocketContext::getClientReplyContext() const return (clientStreamNode *)http->client_stream.tail->prev->data; } +ConnStateData * +ClientSocketContext::getConn() const +{ + return http->getConn(); +} + /** * This routine should be called to grow the inbuf and then * call comm_read(). @@ -384,22 +422,28 @@ ClientSocketContext::writeControlMsg(HttpControlMsg &msg) const HttpReply::Pointer rep(msg.reply); Must(rep != NULL); + // remember the callback + cbControlMsgSent = msg.cbSuccess; + + AsyncCall::Pointer call = commCbCall(33, 5, "ClientSocketContext::wroteControlMsg", + CommIoCbPtrFun(&WroteControlMsg, this)); + + if (http->flags.ftp) { + FtpWriteForwardedReply(this, rep.getRaw(), call); + return; + } + // apply selected clientReplyContext::buildReplyHeader() mods // it is not clear what headers are required for control messages rep->header.removeHopByHopEntries(); rep->header.putStr(HDR_CONNECTION, "keep-alive"); httpHdrMangleList(&rep->header, http->request, ROR_REPLY); - // remember the callback - cbControlMsgSent = msg.cbSuccess; - MemBuf *mb = rep->pack(); debugs(11, 2, "HTTP Client " << clientConnection); debugs(11, 2, "HTTP Client CONTROL MSG:\n---------\n" << mb->buf << "\n----------"); - AsyncCall::Pointer call = commCbCall(33, 5, "ClientSocketContext::wroteControlMsg", - CommIoCbPtrFun(&WroteControlMsg, this)); Comm::Write(clientConnection, mb, call); delete mb; @@ -834,6 +878,8 @@ ConnStateData::~ConnStateData() assert(this != NULL); debugs(33, 3, HERE << clientConnection); + FtpCloseDataConnection(this); + if (isOpen()) debugs(33, DBG_IMPORTANT, "BUG: ConnStateData did not close " << clientConnection); @@ -1442,6 +1488,7 @@ static void clientSocketRecipient(clientStreamNode * node, ClientHttpRequest * http, HttpReply * rep, StoreIOBuffer receivedData) { + debugs(33,7, HERE << "rep->content_length=" << (rep ? rep->content_length : -2) << " receivedData.length=" << receivedData.length); /* Test preconditions */ assert(node != NULL); PROF_start(clientSocketRecipient); @@ -1464,6 +1511,13 @@ clientSocketRecipient(clientStreamNode * node, ClientHttpRequest * http, return; } + if (http->getConn()->isFtp) { + assert(context->http == http); + FtpHandleReply(context.getRaw(), rep, receivedData); + PROF_stop(clientSocketRecipient); + return; + } + // After sending Transfer-Encoding: chunked (at least), always send // the last-chunk if there was no error, ignoring responseFinishedOrFailed. const bool mustSendLastChunk = http->request->flags.chunkedReply && @@ -2623,6 +2677,10 @@ clientProcessRequest(ConnStateData *conn, HttpParser *hp, ClientSocketContext *c goto finish; } + if (conn->isFtp) { + assert(http->request); + request = http->request; + } else if ((request = HttpRequest::CreateFromUrlAndMethod(http->uri, method)) == NULL) { clientStreamNode *node = context->getClientReplyContext(); debugs(33, 5, "Invalid URL: " << http->uri); @@ -2660,7 +2718,8 @@ clientProcessRequest(ConnStateData *conn, HttpParser *hp, ClientSocketContext *c /* compile headers */ /* we should skip request line! */ /* XXX should actually know the damned buffer size here */ - if (http_ver.major >= 1 && !request->parseHeader(HttpParserHdrBuf(hp), HttpParserHdrSz(hp))) { + if (!conn->isFtp && http_ver.major >= 1 && + !request->parseHeader(HttpParserHdrBuf(hp), HttpParserHdrSz(hp))) { clientStreamNode *node = context->getClientReplyContext(); debugs(33, 5, "Failed to parse request headers:\n" << HttpParserHdrBuf(hp)); conn->quitAfterError(request.getRaw()); @@ -2781,8 +2840,10 @@ clientProcessRequest(ConnStateData *conn, HttpParser *hp, ClientSocketContext *c } } - http->request = request.getRaw(); - HTTPMSGLOCK(http->request); + if (!conn->isFtp) { + http->request = request.getRaw(); + HTTPMSGLOCK(http->request); + } clientSetKeepaliveFlag(http); // Let tunneling code be fully responsible for CONNECT requests @@ -2867,7 +2928,8 @@ connStripBufferWhitespace (ConnStateData * conn) static int connOkToAddRequest(ConnStateData * conn) { - int result = conn->getConcurrentRequestCount() < (Config.onoff.pipeline_prefetch ? 2 : 1); + const int limit = !conn->isFtp && Config.onoff.pipeline_prefetch ? 2 : 1; + const int result = conn->getConcurrentRequestCount() < limit; if (!result) { debugs(33, 3, HERE << conn->clientConnection << " max concurrent requests reached"); @@ -2908,14 +2970,18 @@ ConnStateData::clientParseRequests() /* Terminate the string */ in.buf[in.notYetUsed] = '\0'; - /* Begin the parsing */ - PROF_start(parseHttpRequest); - HttpParserInit(&parser_, in.buf, in.notYetUsed); - - /* Process request */ Http::ProtocolVersion http_ver; - ClientSocketContext *context = parseHttpRequest(this, &parser_, &method, &http_ver); - PROF_stop(parseHttpRequest); + ClientSocketContext *context = NULL; + if (!isFtp) { + /* Begin the parsing */ + PROF_start(parseHttpRequest); + HttpParserInit(&parser_, in.buf, in.notYetUsed); + + /* Process request */ + context = parseHttpRequest(this, &parser_, &method, &http_ver); + PROF_stop(parseHttpRequest); + } else + context = FtpParseRequest(this, &method, &http_ver); /* partial or incomplete request */ if (!context) { @@ -3063,7 +3129,7 @@ ConnStateData::handleReadData(char *buf, size_t size) /** * called when new request body data has been buffered in in.buf * may close the connection if we were closing and piped everything out - * +e * * \retval false called comm_close or setReplyToError (the caller should bail) * \retval true we did not call comm_close or setReplyToError */ @@ -3296,7 +3362,7 @@ clientLifetimeTimeout(const CommTimeoutCbParams &io) ConnStateData * connStateCreate(const Comm::ConnectionPointer &client, AnyP::PortCfg *port) { - ConnStateData *result = new ConnStateData; + ConnStateData *result = new ConnStateData(port->protocol); result->clientConnection = client; result->log_addr = client->remote; @@ -3349,7 +3415,9 @@ connStateCreate(const Comm::ConnectionPointer &client, AnyP::PortCfg *port) clientdbEstablished(client->remote, 1); - result->flags.readMore = true; + if (!result->isFtp) + result->flags.readMore = true; + return result; } @@ -3705,6 +3773,33 @@ httpsAccept(const CommAcceptCbParams ¶ms) } } +/** handle a new FTP connection */ +static void +ftpAccept(const CommAcceptCbParams ¶ms) +{ + AnyP::PortCfg *s = static_cast(params.data); + + if (params.flag != COMM_OK) { + // Its possible the call was still queued when the client disconnected + debugs(33, 2, "ftpAccept: " << s->listenConn << ": accept failure: " << xstrerr(params.xerrno)); + return; + } + + debugs(33, 4, HERE << params.conn << ": accepted"); + fd_note(params.conn->fd, "client ftp connect"); + + if (s->tcp_keepalive.enabled) { + commSetTcpKeepalive(params.conn->fd, s->tcp_keepalive.idle, s->tcp_keepalive.interval, s->tcp_keepalive.timeout); + } + + ++incoming_sockets_accepted; + + // Socket is ready, setup the connection manager to start using it + ConnStateData *connState = connStateCreate(params.conn, s); + + FtpWriteGreeting(connState); +} + void ConnStateData::sslCrtdHandleReplyWrapper(void *data, const HelperReply &reply) { @@ -4129,6 +4224,38 @@ clientHttpsConnectionsOpen(void) } #endif +static void +clientFtpConnectionsOpen(void) +{ + AnyP::PortCfg *s; + + for (s = Config.Sockaddr.ftp; s; s = s->next) { + if (MAXTCPLISTENPORTS == NHttpSockets) { + debugs(1, DBG_IMPORTANT, "Ignoring 'ftp_port' lines exceeding the limit."); + debugs(1, DBG_IMPORTANT, "The limit is " << MAXTCPLISTENPORTS << " FTP ports."); + continue; + } + + // Fill out a Comm::Connection which IPC will open as a listener for us + s->listenConn = new Comm::Connection; + s->listenConn->local = s->s; + s->listenConn->flags = COMM_NONBLOCKING | (s->flags.tproxyIntercept ? COMM_TRANSPARENT : 0) | + (s->flags.natIntercept ? COMM_INTERCEPTION : 0); + + // setup the subscriptions such that new connections accepted by listenConn are handled by FTP + typedef CommCbFunPtrCallT AcceptCall; + RefCount subCall = commCbCall(5, 5, "ftpAccept", CommAcceptCbPtrFun(ftpAccept, s)); + Subscription::Pointer sub = new CallSubscription(subCall); + + AsyncCall::Pointer listenCall = asyncCall(33, 2, "clientListenerConnectionOpened", + ListeningStartedDialer(&clientListenerConnectionOpened, + s, Ipc::fdnFtpSocket, sub)); + Ipc::StartListening(SOCK_STREAM, IPPROTO_TCP, s->listenConn, Ipc::fdnFtpSocket, listenCall); + HttpSockets[NHttpSockets] = -1; + ++NHttpSockets; + } +} + /// process clientHttpConnectionsOpen result static void clientListenerConnectionOpened(AnyP::PortCfg *s, const Ipc::FdNoteId portTypeNote, const Subscription::Pointer &sub) @@ -4160,13 +4287,14 @@ clientOpenListenSockets(void) #if USE_SSL clientHttpsConnectionsOpen(); #endif + clientFtpConnectionsOpen(); if (NHttpSockets < 1) - fatal("No HTTP or HTTPS ports configured"); + fatal("No HTTP, HTTPS or FTP ports configured"); } void -clientHttpConnectionsClose(void) +clientConnectionsClose(void) { for (AnyP::PortCfg *s = Config.Sockaddr.http; s; s = s->next) { if (s->listenConn != NULL) { @@ -4186,6 +4314,14 @@ clientHttpConnectionsClose(void) } #endif + for (AnyP::PortCfg *s = Config.Sockaddr.ftp; s; s = s->next) { + if (s->listenConn != NULL) { + debugs(1, DBG_IMPORTANT, "Closing FTP port " << s->listenConn->local); + s->listenConn->close(); + s->listenConn = NULL; + } + } + // TODO see if we can drop HttpSockets array entirely */ for (int i = 0; i < NHttpSockets; ++i) { HttpSockets[i] = -1; @@ -4274,8 +4410,9 @@ clientAclChecklistCreate(const acl_access * acl, ClientHttpRequest * http) CBDATA_CLASS_INIT(ConnStateData); -ConnStateData::ConnStateData() : +ConnStateData::ConnStateData(const char *protocol) : AsyncJob("ConnStateData"), + isFtp(strcmp(protocol, "ftp") == 0), #if USE_SSL sslBumpMode(Ssl::bumpEnd), switchedToHttps_(false), @@ -4453,6 +4590,14 @@ ConnStateData::clientPinnedConnectionClosed(const CommCloseCbParams &io) debugs(33, 3, "Closing client connection on pinned zero reply."); clientConnection->close(); } + if (isFtp) { + // XXX + /* + debugs(33, 5, HERE << "FTP server connection closed, closing client " + "connection."); + clientConnection->close(); + */ + } } void @@ -4555,3 +4700,577 @@ ConnStateData::unpinConnection() /* NOTE: pinning.pinned should be kept. This combined with fd == -1 at the end of a request indicates that the host * connection has gone away */ } + + +static void +FtpAcceptDataConnection(const CommAcceptCbParams ¶ms) +{ + ConnStateData *connState = static_cast(params.data); + + if (params.flag != COMM_OK) { + // Its possible the call was still queued when the client disconnected + debugs(33, 2, HERE << connState->ftp.dataListenConn << ": accept " + "failure: " << xstrerr(params.xerrno)); + return; + } + + debugs(33, 4, HERE << params.conn << ": accepted"); + fd_note(params.conn->fd, "client ftp data connect"); + ++incoming_sockets_accepted; + + FtpCloseDataConnection(connState); + connState->ftp.dataConn = params.conn; +} + +static void +FtpCloseDataConnection(ConnStateData *conn) +{ + if (Comm::IsConnOpen(conn->ftp.dataListenConn)) { + debugs(33, 5, HERE << "FTP closing client data listen socket: " << + *conn->ftp.dataListenConn); + conn->ftp.dataListenConn->close(); + } + conn->ftp.dataListenConn = NULL; + + if (Comm::IsConnOpen(conn->ftp.dataConn)) { + debugs(33, 5, HERE << "FTP closing client data connection: " << + *conn->ftp.dataConn); + conn->ftp.dataConn->close(); + } + conn->ftp.dataConn = NULL; +} + +static void +FtpWriteGreeting(ConnStateData *conn) +{ + MemBuf mb; + const String msg = "220 Service ready\r\n"; + mb.init(msg.size(), msg.size()); + mb.append(msg.rawBuf(), msg.size()); + + AsyncCall::Pointer call = commCbCall(33, 5, "FtpWroteEarlyReply", + CommIoCbPtrFun(&FtpWroteEarlyReply, conn)); + Comm::Write(conn->clientConnection, &mb, call); +} + +static void +FtpWriteEarlyReply(ConnStateData *connState, const int code, const char *msg) +{ + debugs(33, 7, HERE << code << ' ' << msg); + assert(99 < code && code < 1000); + + MemBuf mb; + mb.init(); + mb.Printf("%i %s\r\n", code, msg); + + AsyncCall::Pointer call = commCbCall(33, 5, "FtpWroteEarlyReply", + CommIoCbPtrFun(&FtpWroteEarlyReply, connState)); + Comm::Write(connState->clientConnection, &mb, call); + + connState->flags.readMore = false; +} + +static void +FtpWriteReply(ClientSocketContext *context, MemBuf &mb) +{ + AsyncCall::Pointer call = commCbCall(33, 5, "FtpWroteReply", + CommIoCbPtrFun(&FtpWroteReply, context)); + Comm::Write(context->clientConnection, &mb, call); +} + +static void +FtpWriteCustomReply(ClientSocketContext *context, const int code, const char *msg, const HttpReply *reply) +{ + debugs(33, 7, HERE << code << ' ' << msg); + assert(99 < code && code < 1000); + + const bool sendDetails = reply != NULL && + reply->header.has(HDR_FTP_STATUS) && reply->header.has(HDR_FTP_REASON); + + MemBuf mb; + mb.init(); + if (sendDetails) { + mb.Printf("%i-%s\r\n", code, msg); + mb.Printf(" Server reply:\r\n"); + FtpPrintReply(mb, reply, " "); + mb.Printf("%i \r\n", code); + } else + mb.Printf("%i %s\r\n", code, msg); + + FtpWriteReply(context, mb); +} + +/** Parse an FTP request + * + * \note Sets result->flags.parsed_ok to 0 if failed to parse the request, + * to 1 if the request was correctly parsed. + * \param[in] connState a ConnStateData. The caller must make sure it is not null + * \param[out] mehtod_p will be set as a side-effect of the parsing. + * Pointed-to value will be set to Http::METHOD_NONE in case of + * parsing failure + * \param[out] http_ver will be set as a side-effect of the parsing + * \return NULL on incomplete requests, + * a ClientSocketContext structure on success or failure. + */ +static ClientSocketContext * +FtpParseRequest(ConnStateData *connState, HttpRequestMethod *method_p, Http::ProtocolVersion *http_ver) +{ + *method_p = Http::METHOD_GET; + *http_ver = Http::ProtocolVersion(1, 1); + + const char *const eor = + static_cast(memchr(connState->in.buf, '\n', + min(connState->in.notYetUsed, Config.maxRequestHeaderSize))); + + if (eor == NULL && connState->in.notYetUsed >= Config.maxRequestHeaderSize) { + connState->ftp.state = ConnStateData::FTP_ERROR; + FtpWriteEarlyReply(connState, 421, "Too large request"); + return NULL; + } + + if (eor == NULL) { + debugs(33, 5, HERE << "Incomplete request, waiting for end of request"); + return NULL; + } + + // skip leading whitespaces + const char *boc = connState->in.buf; + while (boc < eor && isspace(*boc)) ++boc; + if (boc >= eor) { + debugs(33, 5, HERE << "Empty request, ignoring"); + connNoteUseOfBuffer(connState, eor + 1 - connState->in.buf); + return NULL; + } + + const char *eoc = boc; + while (eoc < eor && !isspace(*eoc)) ++eoc; + connState->in.buf[eoc - connState->in.buf] = '\0'; + + const char *bop = eoc + 1; + while (bop < eor && isspace(*bop)) ++bop; + if (bop < eor) { + const char *eop = eor - 1; + while (isspace(*eop)) --eop; + assert(eop >= bop); + connState->in.buf[eop + 1 - connState->in.buf] = '\0'; + } else + bop = NULL; + + debugs(33, 7, HERE << "Parsed FTP command " << boc << " with " << + (bop == NULL ? "no " : "") << "parameters" << + (bop != NULL ? ": " : "") << bop); + + String cmd = boc; + String params = bop; + + // the first command must be USER + if (connState->ftp.uri.size() == 0 && cmd.caseCmp("USER") != 0) { + debugs(33, 5, HERE << "Unexpected FTP command: expected USER, got " << + boc); + FtpWriteEarlyReply(connState, 530, "Must login first"); + connNoteUseOfBuffer(connState, eor + 1 - connState->in.buf); + return NULL; + } + + if (!FtpHandleRequest(connState, cmd, params)) { + connNoteUseOfBuffer(connState, eor + 1 - connState->in.buf); + return NULL; + } + + char *uri = xstrdup(connState->ftp.uri.termedBuf()); + HttpRequest *const request = + HttpRequest::CreateFromUrlAndMethod(uri, *method_p); + if (request == NULL) { + debugs(33, 5, HERE << "Invalid FTP URL: " << connState->ftp.uri); + FtpWriteEarlyReply(connState, 501, "Invalid host"); + connState->ftp.uri.clean(); + connNoteUseOfBuffer(connState, eor + 1 - connState->in.buf); + safe_free(uri); + return NULL; + } + + request->header.putStr(HDR_FTP_COMMAND, cmd.termedBuf()); + if (params.size() > 0) + request->header.putStr(HDR_FTP_ARGUMENTS, params.termedBuf()); + + ClientHttpRequest *const http = new ClientHttpRequest(connState); + http->request = request; + HTTPMSGLOCK(http->request); + http->req_sz = eor - connState->in.buf + 1; + http->flags.ftp = true; + http->uri = xstrdup(connState->ftp.uri.termedBuf()); + + ClientSocketContext *const result = + ClientSocketContextNew(connState->clientConnection, http); + + StoreIOBuffer tempBuffer; + tempBuffer.data = result->reqbuf; + tempBuffer.length = HTTP_REQBUF_SZ; + + ClientStreamData newServer = new clientReplyContext(http); + ClientStreamData newClient = result; + clientStreamInit(&http->client_stream, clientGetMoreData, clientReplyDetach, + clientReplyStatus, newServer, clientSocketRecipient, + clientSocketDetach, newClient, tempBuffer); + + result->flags.parsed_ok = 1; + connState->flags.readMore = false; + return result; +} + +static void +FtpHandleReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data) +{ + if (reply == NULL || reply->content_length == -1) { + FtpHandleReplyData(context, data); + return; + } + + assert(reply != NULL); + assert(reply->content_length == 0); + assert(context->startOfOutput()); + + const ConnStateData::FtpState state = context->getConn()->ftp.state; + assert(state != ConnStateData::FTP_BEGIN); + + static FtpReplyHandler *handlers[] = { + NULL, // FTP_BEGIN + NULL, // FTP_CONNECTED + FtpHandlePasvReply, // FTP_HANDLE_PASV + NULL, // FTP_HANDLE_DATA_REQUEST + FtpHandleErrorReply // FTP_ERROR + }; + FtpReplyHandler *const handler = handlers[state]; + if (handler) + (*handler)(context, reply); + else + FtpWriteForwardedReply(context, reply); +} + +static void +FtpHandlePasvReply(ClientSocketContext *context, const HttpReply *reply) +{ + if (context->http->request->errType != ERR_NONE) { + FtpWriteCustomReply(context, 502, "Server does not support PASV", reply); + return; + } + + FtpCloseDataConnection(context->getConn()); + + Comm::ConnectionPointer conn = new Comm::Connection; + conn->flags = COMM_NONBLOCKING; + conn->local = context->clientConnection->local; + conn->local.SetPort(0); + ConnStateData *const connState = context->getConn(); + const char *const note = connState->ftp.uri.termedBuf(); + comm_open_listener(SOCK_STREAM, IPPROTO_TCP, conn, note); + if (!Comm::IsConnOpen(conn)) { + debugs(5, DBG_CRITICAL, HERE << "comm_open_listener failed:" << + conn->local << " error: " << errno); + FtpWriteCustomReply(context, 451, "Internal error"); + return; + } + + typedef CommCbFunPtrCallT AcceptCall; + RefCount subCall = commCbCall(5, 5, "FtpAcceptDataConnection", + CommAcceptCbPtrFun(FtpAcceptDataConnection, connState)); + Subscription::Pointer sub = new CallSubscription(subCall); + AsyncJob::Start(new Comm::TcpAcceptor(conn, note, sub)); + + connState->ftp.dataListenConn = conn; + + char addr[MAX_IPSTRLEN]; + conn->local.NtoA(addr, MAX_IPSTRLEN, AF_INET); + addr[MAX_IPSTRLEN - 1] = '\0'; + for (char *c = addr; *c != '\0'; ++c) { + if (*c == '.') + *c = ','; + } + + const unsigned short port = comm_local_port(conn->fd); + conn->local.SetPort(port); + + MemBuf mb; + mb.init(); + mb.Printf("227 =%s,%i,%i\r\n", addr, static_cast(port >> 8), + static_cast(port % 256)); + + FtpWriteReply(context, mb); +} + +static void +FtpHandleErrorReply(ClientSocketContext *context, const HttpReply *reply) +{ + int code; + ConnStateData *const connState = context->getConn(); + if (!connState->pinning.pinned) { // we failed to connect to server + connState->ftp.uri.clean(); + code = 530; + } else + code = 421; + const char *const msg = err_type_str[context->http->request->errType]; + FtpWriteCustomReply(context, code, msg, reply); +} + +static void +FtpHandleReplyData(ClientSocketContext *context, StoreIOBuffer data) +{ + debugs(33, 7, HERE << data.length); + + ConnStateData *const conn = context->getConn(); + + if (data.length <= 0) { + FtpWroteReplyData(conn->clientConnection, NULL, 0, COMM_OK, 0, context); + return; + } + + if (!Comm::IsConnOpen(conn->ftp.dataConn)) { + debugs(33, 3, HERE << "got FTP reply data when client data connection " + "is closed, ignoring"); + return; + } + + MemBuf mb; + mb.init(data.length, data.length); + mb.append(data.data, data.length); + + AsyncCall::Pointer call = commCbCall(33, 5, "FtpWroteReplyData", + CommIoCbPtrFun(&FtpWroteReplyData, context)); + Comm::Write(conn->ftp.dataConn, &mb, call); + + context->noteSentBodyBytes(data.length); +} + +static void +FtpWroteReplyData(const Comm::ConnectionPointer &conn, char *bufnotused, size_t size, comm_err_t errflag, int xerrno, void *data) +{ + if (errflag == COMM_ERR_CLOSING) + return; + + ClientSocketContext *const context = static_cast(data); + ConnStateData *const connState = context->getConn(); + + if (errflag != COMM_OK) { + debugs(33, 3, HERE << "FTP reply data writing failed: " << + xstrerr(xerrno)); + FtpCloseDataConnection(connState); + FtpWriteCustomReply(context, 426, "Data connection error; transfer aborted"); + return; + } + + switch (context->socketState()) { + case STREAM_NONE: + context->pullData(); + return; + case STREAM_COMPLETE: + debugs(33, 3, HERE << "FTP reply data transfer successfully complete"); + FtpWriteCustomReply(context, 226, "Transfer complete"); + break; + case STREAM_UNPLANNED_COMPLETE: + debugs(33, 3, HERE << "FTP reply data transfer failed: STREAM_UNPLANNED_COMPLETE"); + FtpWriteCustomReply(context, 451, "Server error; transfer aborted"); + break; + case STREAM_FAILED: + debugs(33, 3, HERE << "FTP reply data transfer failed: STREAM_FAILED"); + FtpWriteCustomReply(context, 451, "Server error; transfer aborted"); + break; + default: + fatal("unreachable code"); + } + + FtpCloseDataConnection(connState); +} + +static void +FtpWriteForwardedReply(ClientSocketContext *context, const HttpReply *reply) +{ + const AsyncCall::Pointer call = commCbCall(33, 5, "FtpWroteReply", + CommIoCbPtrFun(&FtpWroteReply, context)); + FtpWriteForwardedReply(context, reply, call); +} + +static void +FtpWriteForwardedReply(ClientSocketContext *context, const HttpReply *reply, AsyncCall::Pointer call) +{ + assert(reply != NULL); + const HttpHeader &header = reply->header; + assert(header.has(HDR_FTP_STATUS)); + assert(header.has(HDR_FTP_REASON)); + + const int status = header.getInt(HDR_FTP_STATUS); + debugs(33, 7, HERE << "status: " << status); + + MemBuf mb; + mb.init(); + FtpPrintReply(mb, reply); + + Comm::Write(context->clientConnection, &mb, call); +} + +static void +FtpPrintReply(MemBuf &mb, const HttpReply *reply, const char *const prefix) +{ + const HttpHeader &header = reply->header; + + char status[4]; + if (header.has(HDR_FTP_STATUS)) + snprintf(status, sizeof(status), "%i", header.getInt(HDR_FTP_STATUS)); + else + status[0] = '\0'; + + HttpHeaderPos pos = HttpHeaderInitPos; + const HttpHeaderEntry *e = header.getEntry(&pos); + while (e) { + const HttpHeaderEntry *const next = header.getEntry(&pos); + if (e->id == HDR_FTP_REASON) { + const bool isLastLine = next == NULL || next->id != HDR_FTP_REASON; + const int separator = status[0] == '\0' || isLastLine ? ' ' : '-'; + mb.Printf("%s%s%c%s\r\n", prefix, status, separator, + e->value.termedBuf()); + } + e = next; + } +} + +static void +FtpWroteEarlyReply(const Comm::ConnectionPointer &conn, char *bufnotused, size_t size, comm_err_t errflag, int xerrno, void *data) +{ + if (errflag == COMM_ERR_CLOSING) + return; + + if (errflag != COMM_OK) { + debugs(33, 3, HERE << "FTP reply writing failed: " << xstrerr(xerrno)); + conn->close(); + return; + } + + ConnStateData *const connState = static_cast(data); + connState->flags.readMore = true; + connState->readSomeData(); +} + +static void +FtpWroteReply(const Comm::ConnectionPointer &conn, char *bufnotused, size_t size, comm_err_t errflag, int xerrno, void *data) +{ + if (errflag == COMM_ERR_CLOSING) + return; + + if (errflag != COMM_OK) { + debugs(33, 3, HERE << "FTP reply writing failed: " << + xstrerr(xerrno)); + conn->close(); + return; + } + + ClientSocketContext *const context = + static_cast(data); + ConnStateData *const connState = context->getConn(); + + if (connState->ftp.state == ConnStateData::FTP_ERROR) { + conn->close(); + return; + } + + assert(context->socketState() == STREAM_COMPLETE); + connState->flags.readMore = true; + connState->ftp.state = ConnStateData::FTP_CONNECTED; + context->keepaliveNextRequest(); +} + +bool +FtpHandleRequest(ConnStateData *connState, String &cmd, String ¶ms) { + static std::pair handlers[] = { + std::make_pair("PASV", FtpHandlePasvRequest), + std::make_pair("PORT", FtpHandlePortRequest), + std::make_pair("RETR", FtpHandleDataRequest), + std::make_pair("LIST", FtpHandleDataRequest), + std::make_pair("NLST", FtpHandleDataRequest), + std::make_pair("USER", FtpHandleUserRequest) + }; + + FtpRequestHandler *handler = NULL; + for (size_t i = 0; i < sizeof(handlers) / sizeof(*handlers); ++i) { + if (cmd.caseCmp(handlers[i].first) == 0) { + handler = handlers[i].second; + break; + } + } + + return handler != NULL ? (*handler)(connState, cmd, params) : true; +} + +bool +FtpHandleUserRequest(ConnStateData *connState, String &cmd, String ¶ms) +{ + if (params.size() == 0) { + FtpWriteEarlyReply(connState, 501, "Missing username"); + return false; + } + + const String::size_type eou = params.find('@'); + if (eou == String::npos || eou + 1 >= params.size()) { + FtpWriteEarlyReply(connState, 501, "Missing host"); + return false; + } + + static const String scheme = "ftp://"; + const String host = params.substr(eou + 1, params.size()); + String uri = scheme; + uri.append(host); + uri.append("/"); + + if (connState->ftp.uri.size() == 0) + connState->ftp.uri = uri; + else if (uri.caseCmp(connState->ftp.uri) != 0) { + FtpWriteEarlyReply(connState, 501, "Cannot change host"); + return false; + } + + params.cut(eou); + + return true; +} + +bool +FtpHandlePasvRequest(ConnStateData *connState, String &cmd, String ¶ms) +{ + if (params.size() > 0) { + FtpWriteEarlyReply(connState, 501, "Unexpected parameter"); + return false; + } + + connState->ftp.state = ConnStateData::FTP_HANDLE_PASV; + + return true; +} + +bool +FtpHandlePortRequest(ConnStateData *connState, String &cmd, String ¶ms) +{ + FtpWriteEarlyReply(connState, 502, "Command not supported"); + return false; +} + +bool +FtpHandleDataRequest(ConnStateData *connState, String &cmd, String ¶ms) +{ + if (!FtpCheckDataConnection(connState)) + return false; + + connState->ftp.state = ConnStateData::FTP_HANDLE_DATA_REQUEST; + + return true; +} + +bool +FtpCheckDataConnection(ConnStateData *connState) +{ + if (Comm::IsConnOpen(connState->ftp.dataConn)) + return true; + + if (!Comm::IsConnOpen(connState->ftp.dataListenConn)) + FtpWriteEarlyReply(connState, 425, "Use PASV first"); + else + FtpWriteEarlyReply(connState, 425, "Data connection is not established"); + return false; +} diff --git a/src/client_side.h b/src/client_side.h index 208b3bf361..c75a3026d4 100644 --- a/src/client_side.h +++ b/src/client_side.h @@ -138,6 +138,7 @@ public: void buildRangeHeader(HttpReply * rep); clientStreamNode * getTail() const; clientStreamNode * getClientReplyContext() const; + ConnStateData *getConn() const; void connIsFinished(); void removeFromConnectionList(ConnStateData * conn); void deferRecipientForLater(clientStreamNode * node, HttpReply * rep, StoreIOBuffer receivedData); @@ -194,7 +195,7 @@ class ConnStateData : public BodyProducer, public HttpControlMsgSink public: - ConnStateData(); + ConnStateData(const char *protocol); ~ConnStateData(); void readSomeData(); @@ -331,6 +332,22 @@ public: /// the client-side-detected error response instead of getting stuck. void quitAfterError(HttpRequest *request); // meant to be private + const bool isFtp; + enum FtpState { + FTP_BEGIN, + FTP_CONNECTED, + FTP_HANDLE_PASV, + FTP_HANDLE_DATA_REQUEST, + FTP_ERROR + }; + struct { + String uri; + FtpState state; + Comm::ConnectionPointer dataListenConn; + Comm::ConnectionPointer dataConn; + Ip::Address serverDataAddr; + } ftp; + #if USE_SSL /// called by FwdState when it is done bumping the server void httpsPeeked(Comm::ConnectionPointer serverConnection); @@ -418,7 +435,7 @@ const char *findTrailingHTTPVersion(const char *uriAndHTTPVersion, const char *e int varyEvaluateMatch(StoreEntry * entry, HttpRequest * req); void clientOpenListenSockets(void); -void clientHttpConnectionsClose(void); +void clientConnectionsClose(void); void httpRequestFree(void *); #endif /* SQUID_CLIENTSIDE_H */ diff --git a/src/client_side_request.h b/src/client_side_request.h index eaa5f9211e..9c7d58f62c 100644 --- a/src/client_side_request.h +++ b/src/client_side_request.h @@ -124,6 +124,7 @@ public: bool internal; bool done_copying; bool purging; + bool ftp; } flags; struct { diff --git a/src/forward.cc b/src/forward.cc index d872d347c4..a9de6934dd 100644 --- a/src/forward.cc +++ b/src/forward.cc @@ -49,6 +49,7 @@ #include "fde.h" #include "forward.h" #include "ftp.h" +#include "FtpGatewayServer.h" #include "globals.h" #include "gopher.h" #include "hier_code.h" @@ -1045,6 +1046,13 @@ FwdState::connectDone(const Comm::ConnectionPointer &conn, comm_err_t status, in } #endif + const CbcPointer &clientConnState = + request->clientConnectionManager; + if (clientConnState->isFtp) { + clientConnState->pinConnection(serverConnection(), request, + serverConnection()->getPeer(), false); + } + dispatch(); } @@ -1298,7 +1306,10 @@ FwdState::dispatch() break; case AnyP::PROTO_FTP: - ftpStart(this); + if (request->clientConnectionManager->isFtp) + ftpGatewayServerStart(this); + else + ftpStart(this); break; case AnyP::PROTO_CACHE_OBJECT: diff --git a/src/ftp.cc b/src/ftp.cc index e031886aee..d2732badaf 100644 --- a/src/ftp.cc +++ b/src/ftp.cc @@ -32,15 +32,14 @@ #include "squid.h" #include "comm.h" -#include "comm/ConnOpener.h" #include "comm/TcpAcceptor.h" -#include "comm/Write.h" #include "CommCalls.h" #include "compat/strtoll.h" #include "errorpage.h" #include "fd.h" #include "fde.h" #include "forward.h" +#include "FtpServer.h" #include "html_quote.h" #include "HttpHdrContRange.h" #include "HttpHeader.h" @@ -77,9 +76,6 @@ \ingroup ServerProtocolFTPAPI */ -/// \ingroup ServerProtocolFTPInternal -static const char *const crlf = "\r\n"; - #define CTRL_BUFLEN 1024 /// \ingroup ServerProtocolFTPInternal static char cbuf[CTRL_BUFLEN]; @@ -148,39 +144,8 @@ class FtpStateData; /// \ingroup ServerProtocolFTPInternal typedef void (FTPSM) (FtpStateData *); -/// common code for FTP control and data channels -/// does not own the channel descriptor, which is managed by FtpStateData -class FtpChannel -{ -public: - FtpChannel() {}; - - /// called after the socket is opened, sets up close handler - void opened(const Comm::ConnectionPointer &conn, const AsyncCall::Pointer &aCloser); - - /** Handles all operations needed to properly close the active channel FD. - * clearing the close handler, clearing the listen socket properly, and calling comm_close - */ - void close(); - - void clear(); ///< just drops conn and close handler. does not close active connections. - - Comm::ConnectionPointer conn; ///< channel descriptor - - /** A temporary handle to the connection being listened on. - * Closing this will also close the waiting Data channel acceptor. - * If a data connection has already been accepted but is still waiting in the event queue - * the callback will still happen and needs to be handled (usually dropped). - */ - Comm::ConnectionPointer listenConn; - - AsyncCall::Pointer opener; ///< Comm opener handler callback. -private: - AsyncCall::Pointer closer; ///< Comm close handler callback -}; - /// \ingroup ServerProtocolFTPInternal -class FtpStateData : public ServerStateData +class FtpStateData : public Ftp::ServerStateData { public: @@ -188,8 +153,8 @@ public: void operator delete (void *); void *toCbdata() { return this; } - FtpStateData(FwdState *, const Comm::ConnectionPointer &conn); - ~FtpStateData(); + FtpStateData(FwdState *fwdState); + virtual ~FtpStateData(); char user[MAX_URL]; char password[MAX_URL]; int password_url; @@ -200,7 +165,6 @@ public: String base_href; int conn_att; int login_att; - ftp_state_t state; time_t mdtm; int64_t theSize; wordlist *pathcomps; @@ -210,32 +174,10 @@ public: char *proxy_host; size_t list_width; String cwd_message; - char *old_request; - char *old_reply; char *old_filepath; char typecode; MemBuf listing; ///< FTP directory listing in HTML format. - // \todo: optimize ctrl and data structs member order, to minimize size - /// FTP control channel info; the channel is opened once per transaction - struct CtrlChannel: public FtpChannel { - char *buf; - size_t size; - size_t offset; - wordlist *message; - char *last_command; - char *last_reply; - int replycode; - } ctrl; - - /// FTP data channel info; the channel may be opened/closed a few times - struct DataChannel: public FtpChannel { - MemBuf *readBuf; - char *host; - unsigned short port; - bool read_pending; - } data; - struct _ftp_flags flags; private: @@ -244,67 +186,47 @@ private: public: // these should all be private virtual void start(); + virtual Http::StatusCode failedHttpStatus(err_type &error); void loginParser(const char *, int escaped); int restartable(); void appendSuccessHeader(); void hackShortcut(FTPSM * nextState); - void failed(err_type, int xerrno); - void failedErrorMessage(err_type, int xerrno); void unhack(); - void scheduleReadControlReply(int); - void handleControlReply(); void readStor(); void parseListing(); MemBuf *htmlifyListEntry(const char *line); void completedListing(void); - void dataComplete(); - void dataRead(const CommIoCbParams &io); - /// ignore timeout on CTRL channel. set read timeout on DATA channel. - void switchTimeoutToDataChannel(); /// create a data channel acceptor and start listening. - void listenForDataChannel(const Comm::ConnectionPointer &conn, const char *note); + void listenForDataChannel(const Comm::ConnectionPointer &conn); int checkAuth(const HttpHeader * req_hdr); void checkUrlpath(); void buildTitleUrl(); void writeReplyBody(const char *, size_t len); void printfReplyBody(const char *fmt, ...); - virtual const Comm::ConnectionPointer & dataConnection() const; - virtual void maybeReadVirginBody(); - virtual void closeServer(); virtual void completeForwarding(); - virtual void abortTransaction(const char *reason); void processHeadResponse(); void processReplyBody(); - void writeCommand(const char *buf); void setCurrentOffset(int64_t offset) { currentOffset = offset; } int64_t getCurrentOffset() const { return currentOffset; } - static CNCB ftpPasvCallback; + virtual void dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t err, int xerrno); static PF ftpDataWrite; - void ftpTimeout(const CommTimeoutCbParams &io); - void ctrlClosed(const CommCloseCbParams &io); - void dataClosed(const CommCloseCbParams &io); - void ftpReadControlReply(const CommIoCbParams &io); - void ftpWriteCommandCallback(const CommIoCbParams &io); + virtual void timeout(const CommTimeoutCbParams &io); void ftpAcceptDataConnection(const CommAcceptCbParams &io); static HttpReply *ftpAuthRequired(HttpRequest * request, const char *realm); const char *ftpRealm(void); void loginFailed(void); - static wordlist *ftpParseControlReply(char *, size_t, int *, size_t *); - - // sending of the request body to the server - virtual void sentRequestBody(const CommIoCbParams&); - virtual void doneSendingRequestBody(); virtual void haveParsedReplyHeaders(); - virtual bool doneWithServer() const; virtual bool haveControlChannel(const char *caller_name) const; - AsyncCall::Pointer dataCloser(); /// creates a Comm close callback - AsyncCall::Pointer dataOpener(); /// creates a Comm connect callback + +protected: + virtual void handleControlReply(); + virtual void dataClosed(const CommCloseCbParams &io); private: // BodyConsumer for HTTP: consume request body. @@ -460,26 +382,11 @@ FTPSM *FTP_SM_FUNCS[] = { ftpReadMkdir /* SENT_MKDIR */ }; -/// handler called by Comm when FTP control channel is closed unexpectedly -void -FtpStateData::ctrlClosed(const CommCloseCbParams &io) -{ - debugs(9, 4, HERE); - ctrl.clear(); - mustStop("FtpStateData::ctrlClosed"); -} - /// handler called by Comm when FTP data channel is closed unexpectedly void FtpStateData::dataClosed(const CommCloseCbParams &io) { - debugs(9, 4, HERE); - if (data.listenConn != NULL) { - data.listenConn->close(); - data.listenConn = NULL; - // NP clear() does the: data.fd = -1; - } - data.clear(); + Ftp::ServerStateData::dataClosed(io); failed(ERR_FTP_FAILURE, 0); /* failed closes ctrl.conn and frees ftpState */ @@ -489,12 +396,11 @@ FtpStateData::dataClosed(const CommCloseCbParams &io) */ } -FtpStateData::FtpStateData(FwdState *theFwdState, const Comm::ConnectionPointer &conn) : AsyncJob("FtpStateData"), ServerStateData(theFwdState) +FtpStateData::FtpStateData(FwdState *fwdState): AsyncJob("FtpStateData"), + Ftp::ServerStateData(fwdState) { const char *url = entry->url(); debugs(9, 3, HERE << "'" << url << "'" ); - ++ statCounter.server.all.requests; - ++ statCounter.server.ftp.requests; theSize = -1; mdtm = -1; @@ -503,10 +409,6 @@ FtpStateData::FtpStateData(FwdState *theFwdState, const Comm::ConnectionPointer flags.rest_supported = 1; - typedef CommCbMemFunT Dialer; - AsyncCall::Pointer closer = JobCallback(9, 5, Dialer, this, FtpStateData::ctrlClosed); - ctrl.opened(conn, closer); - if (request->method == Http::METHOD_PUT) flags.put = 1; } @@ -515,50 +417,21 @@ FtpStateData::~FtpStateData() { debugs(9, 3, HERE << entry->url() ); - if (reply_hdr) { - memFree(reply_hdr, MEM_8K_BUF); - reply_hdr = NULL; - } - - if (data.opener != NULL) { - data.opener->cancel("FtpStateData destructed"); - data.opener = NULL; - } - data.close(); - if (Comm::IsConnOpen(ctrl.conn)) { - debugs(9, DBG_IMPORTANT, HERE << "Internal bug: FtpStateData left " << - "open control channel " << ctrl.conn); - } - - if (ctrl.buf) { - memFreeBuf(ctrl.size, ctrl.buf); - ctrl.buf = NULL; + debugs(9, DBG_IMPORTANT, HERE << "Internal bug: Ftp::ServerStateData " + "left open control channel " << ctrl.conn); } - if (data.readBuf) { - if (!data.readBuf->isNull()) - data.readBuf->clean(); - - delete data.readBuf; + if (reply_hdr) { + memFree(reply_hdr, MEM_8K_BUF); + reply_hdr = NULL; } if (pathcomps) wordlistDestroy(&pathcomps); - if (ctrl.message) - wordlistDestroy(&ctrl.message); - cwd_message.clean(); - safe_free(ctrl.last_reply); - - safe_free(ctrl.last_command); - - safe_free(old_request); - - safe_free(old_reply); - safe_free(old_filepath); title_url.clean(); @@ -568,10 +441,6 @@ FtpStateData::~FtpStateData() safe_free(filepath); safe_free(dirpath); - - safe_free(data.host); - - fwd = NULL; // refcounted } /** @@ -631,22 +500,8 @@ FtpStateData::loginParser(const char *login, int escaped) debugs(9, 9, HERE << ": OUT: login='" << login << "', escaped=" << escaped << ", user=" << user << ", password=" << password); } -/** - * Cancel the timeout on the Control socket and establish one - * on the data socket - */ -void -FtpStateData::switchTimeoutToDataChannel() -{ - commUnsetConnTimeout(ctrl.conn); - - typedef CommCbMemFunT TimeoutDialer; - AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); - commSetConnTimeout(data.conn, Config.Timeout.read, timeoutCall); -} - void -FtpStateData::listenForDataChannel(const Comm::ConnectionPointer &conn, const char *note) +FtpStateData::listenForDataChannel(const Comm::ConnectionPointer &conn) { assert(!Comm::IsConnOpen(data.conn)); @@ -654,6 +509,7 @@ FtpStateData::listenForDataChannel(const Comm::ConnectionPointer &conn, const ch typedef AsyncCallT AcceptCall; RefCount call = static_cast(JobCallback(11, 5, AcceptDialer, this, FtpStateData::ftpAcceptDataConnection)); Subscription::Pointer sub = new CallSubscription(call); + const char *note = entry->url(); /* open the conn if its not already open */ if (!Comm::IsConnOpen(conn)) { @@ -674,17 +530,12 @@ FtpStateData::listenForDataChannel(const Comm::ConnectionPointer &conn, const ch } void -FtpStateData::ftpTimeout(const CommTimeoutCbParams &io) +FtpStateData::timeout(const CommTimeoutCbParams &io) { - debugs(9, 4, HERE << io.conn << ": '" << entry->url() << "'" ); - - if (abortOnBadEntry("entry went bad while waiting for a timeout")) - return; - if (SENT_PASV == state) { /* stupid ftp.netscape.com, of FTP server behind stupid firewall rules */ flags.pasv_supported = false; - debugs(9, DBG_IMPORTANT, "ftpTimeout: timeout in SENT_PASV state" ); + debugs(9, DBG_IMPORTANT, HERE << "timeout in SENT_PASV state"); // cancel the data connection setup. if (data.opener != NULL) { @@ -694,8 +545,7 @@ FtpStateData::ftpTimeout(const CommTimeoutCbParams &io) data.close(); } - failed(ERR_READ_TIMEOUT, 0); - /* failed() closes ctrl.conn and frees ftpState */ + Ftp::ServerStateData::timeout(io); } /// \ingroup ServerProtocolFTPInternal @@ -1148,11 +998,11 @@ FtpStateData::parseListing() line = (char *)memAllocate(MEM_4K_BUF); ++end; s = sbuf; - s += strspn(s, crlf); + s += strspn(s, Ftp::crlf); - for (; s < end; s += strcspn(s, crlf), s += strspn(s, crlf)) { + for (; s < end; s += strcspn(s, Ftp::crlf), s += strspn(s, Ftp::crlf)) { debugs(9, 7, HERE << "s = {" << s << "}"); - linelen = strcspn(s, crlf) + 1; + linelen = strcspn(s, Ftp::crlf) + 1; if (linelen < 2) break; @@ -1182,146 +1032,6 @@ FtpStateData::parseListing() xfree(sbuf); } -const Comm::ConnectionPointer & -FtpStateData::dataConnection() const -{ - return data.conn; -} - -void -FtpStateData::dataComplete() -{ - debugs(9, 3,HERE); - - /* Connection closed; transfer done. */ - - /// Close data channel, if any, to conserve resources while we wait. - data.close(); - - /* expect the "transfer complete" message on the control socket */ - /* - * DPW 2007-04-23 - * Previously, this was the only place where we set the - * 'buffered_ok' flag when calling scheduleReadControlReply(). - * It caused some problems if the FTP server returns an unexpected - * status code after the data command. FtpStateData was being - * deleted in the middle of dataRead(). - */ - /* AYJ: 2011-01-13: Bug 2581. - * 226 status is possibly waiting in the ctrl buffer. - * The connection will hang if we DONT send buffered_ok. - * This happens on all transfers which can be completly sent by the - * server before the 150 started status message is read in by Squid. - * ie all transfers of about one packet hang. - */ - scheduleReadControlReply(1); -} - -void -FtpStateData::maybeReadVirginBody() -{ - // too late to read - if (!Comm::IsConnOpen(data.conn) || fd_table[data.conn->fd].closing()) - return; - - if (data.read_pending) - return; - - const int read_sz = replyBodySpace(*data.readBuf, 0); - - debugs(11,9, HERE << "FTP may read up to " << read_sz << " bytes"); - - if (read_sz < 2) // see http.cc - return; - - data.read_pending = true; - - typedef CommCbMemFunT TimeoutDialer; - AsyncCall::Pointer timeoutCall = JobCallback(9, 5, - TimeoutDialer, this, FtpStateData::ftpTimeout); - commSetConnTimeout(data.conn, Config.Timeout.read, timeoutCall); - - debugs(9,5,HERE << "queueing read on FD " << data.conn->fd); - - typedef CommCbMemFunT Dialer; - entry->delayAwareRead(data.conn, data.readBuf->space(), read_sz, - JobCallback(9, 5, Dialer, this, FtpStateData::dataRead)); -} - -void -FtpStateData::dataRead(const CommIoCbParams &io) -{ - int j; - int bin; - - data.read_pending = false; - - debugs(9, 3, HERE << "ftpDataRead: FD " << io.fd << " Read " << io.size << " bytes"); - - if (io.size > 0) { - kb_incr(&(statCounter.server.all.kbytes_in), io.size); - kb_incr(&(statCounter.server.ftp.kbytes_in), io.size); - } - - if (io.flag == COMM_ERR_CLOSING) - return; - - assert(io.fd == data.conn->fd); - - if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { - abortTransaction("entry aborted during dataRead"); - return; - } - - if (io.flag == COMM_OK && io.size > 0) { - debugs(9,5,HERE << "appended " << io.size << " bytes to readBuf"); - data.readBuf->appended(io.size); -#if USE_DELAY_POOLS - DelayId delayId = entry->mem_obj->mostBytesAllowed(); - delayId.bytesIn(io.size); -#endif - ++ IOStats.Ftp.reads; - - for (j = io.size - 1, bin = 0; j; ++bin) - j >>= 1; - - ++ IOStats.Ftp.read_hist[bin]; - } - - if (io.flag != COMM_OK) { - debugs(50, ignoreErrno(io.xerrno) ? 3 : DBG_IMPORTANT, - "ftpDataRead: read error: " << xstrerr(io.xerrno)); - - if (ignoreErrno(io.xerrno)) { - typedef CommCbMemFunT TimeoutDialer; - AsyncCall::Pointer timeoutCall = JobCallback(9, 5, - TimeoutDialer, this, FtpStateData::ftpTimeout); - commSetConnTimeout(io.conn, Config.Timeout.read, timeoutCall); - - maybeReadVirginBody(); - } else { - failed(ERR_READ_ERROR, 0); - /* failed closes ctrl.conn and frees ftpState */ - return; - } - } else if (io.size == 0) { - debugs(9,3, HERE << "Calling dataComplete() because io.size == 0"); - /* - * DPW 2007-04-23 - * Dangerous curves ahead. This call to dataComplete was - * calling scheduleReadControlReply, handleControlReply, - * and then ftpReadTransferDone. If ftpReadTransferDone - * gets unexpected status code, it closes down the control - * socket and our FtpStateData object gets destroyed. As - * a workaround we no longer set the 'buffered_ok' flag in - * the scheduleReadControlReply call. - */ - dataComplete(); - } - - processReplyBody(); -} - void FtpStateData::processReplyBody() { @@ -1519,7 +1229,7 @@ FtpStateData::buildTitleUrl() void ftpStart(FwdState * fwd) { - AsyncJob::Start(new FtpStateData(fwd, fwd->serverConnection())); + AsyncJob::Start(new FtpStateData(fwd)); } void @@ -1539,324 +1249,27 @@ FtpStateData::start() ", path=" << request->urlpath << ", user=" << user << ", passwd=" << password); state = BEGIN; - ctrl.last_command = xstrdup("Connect to server"); - ctrl.buf = (char *)memAllocBuf(4096, &ctrl.size); - ctrl.offset = 0; - data.readBuf = new MemBuf; - data.readBuf->init(4096, SQUID_TCP_SO_RCVBUF); - scheduleReadControlReply(0); -} - -/* ====================================================================== */ - -/// \ingroup ServerProtocolFTPInternal -static char * -escapeIAC(const char *buf) -{ - int n; - char *ret; - unsigned const char *p; - unsigned char *r; - - for (p = (unsigned const char *)buf, n = 1; *p; ++n, ++p) - if (*p == 255) - ++n; - - ret = (char *)xmalloc(n); - - for (p = (unsigned const char *)buf, r=(unsigned char *)ret; *p; ++p) { - *r = *p; - ++r; - if (*p == 255) { - *r = 255; - ++r; - } - } - - *r = '\0'; - ++r; - assert((r - (unsigned char *)ret) == n ); - return ret; + Ftp::ServerStateData::start(); } -void -FtpStateData::writeCommand(const char *buf) -{ - char *ebuf; - /* trace FTP protocol communications at level 2 */ - debugs(9, 2, "ftp<< " << buf); - - if (Config.Ftp.telnet) - ebuf = escapeIAC(buf); - else - ebuf = xstrdup(buf); - - safe_free(ctrl.last_command); - - safe_free(ctrl.last_reply); - - ctrl.last_command = ebuf; - - if (!Comm::IsConnOpen(ctrl.conn)) { - debugs(9, 2, HERE << "cannot send to closing ctrl " << ctrl.conn); - // TODO: assert(ctrl.closer != NULL); - return; - } - - typedef CommCbMemFunT Dialer; - AsyncCall::Pointer call = JobCallback(9, 5, Dialer, this, FtpStateData::ftpWriteCommandCallback); - Comm::Write(ctrl.conn, ctrl.last_command, strlen(ctrl.last_command), call, NULL); - - scheduleReadControlReply(0); -} - -void -FtpStateData::ftpWriteCommandCallback(const CommIoCbParams &io) -{ - - debugs(9, 5, "ftpWriteCommandCallback: wrote " << io.size << " bytes"); - - if (io.size > 0) { - fd_bytes(io.fd, io.size, FD_WRITE); - kb_incr(&(statCounter.server.all.kbytes_out), io.size); - kb_incr(&(statCounter.server.ftp.kbytes_out), io.size); - } - - if (io.flag == COMM_ERR_CLOSING) - return; - - if (io.flag) { - debugs(9, DBG_IMPORTANT, "ftpWriteCommandCallback: " << io.conn << ": " << xstrerr(io.xerrno)); - failed(ERR_WRITE_ERROR, io.xerrno); - /* failed closes ctrl.conn and frees ftpState */ - return; - } -} - -wordlist * -FtpStateData::ftpParseControlReply(char *buf, size_t len, int *codep, size_t *used) -{ - char *s; - char *sbuf; - char *end; - int usable; - int complete = 0; - wordlist *head = NULL; - wordlist *list; - wordlist **tail = &head; - size_t offset; - size_t linelen; - int code = -1; - debugs(9, 3, HERE); - /* - * We need a NULL-terminated buffer for scanning, ick - */ - sbuf = (char *)xmalloc(len + 1); - xstrncpy(sbuf, buf, len + 1); - end = sbuf + len - 1; - - while (*end != '\r' && *end != '\n' && end > sbuf) - --end; - - usable = end - sbuf; - - debugs(9, 3, HERE << "usable = " << usable); - - if (usable == 0) { - debugs(9, 3, HERE << "didn't find end of line"); - safe_free(sbuf); - return NULL; - } - - debugs(9, 3, HERE << len << " bytes to play with"); - ++end; - s = sbuf; - s += strspn(s, crlf); - - for (; s < end; s += strcspn(s, crlf), s += strspn(s, crlf)) { - if (complete) - break; - - debugs(9, 5, HERE << "s = {" << s << "}"); - - linelen = strcspn(s, crlf) + 1; - - if (linelen < 2) - break; - - if (linelen > 3) - complete = (*s >= '0' && *s <= '9' && *(s + 3) == ' '); - - if (complete) - code = atoi(s); - - offset = 0; - - if (linelen > 3) - if (*s >= '0' && *s <= '9' && (*(s + 3) == '-' || *(s + 3) == ' ')) - offset = 4; - - list = new wordlist(); - - list->key = (char *)xmalloc(linelen - offset); - - xstrncpy(list->key, s + offset, linelen - offset); - - /* trace the FTP communication chat at level 2 */ - debugs(9, 2, "ftp>> " << code << " " << list->key); - - *tail = list; - - tail = &list->next; - } - - *used = (size_t) (s - sbuf); - safe_free(sbuf); - - if (!complete) - wordlistDestroy(&head); - - if (codep) - *codep = code; - - return head; -} - -/** - * DPW 2007-04-23 - * Looks like there are no longer anymore callers that set - * buffered_ok=1. Perhaps it can be removed at some point. - */ -void -FtpStateData::scheduleReadControlReply(int buffered_ok) -{ - debugs(9, 3, HERE << ctrl.conn); - - if (buffered_ok && ctrl.offset > 0) { - /* We've already read some reply data */ - handleControlReply(); - } else { - /* - * Cancel the timeout on the Data socket (if any) and - * establish one on the control socket. - */ - if (Comm::IsConnOpen(data.conn)) { - commUnsetConnTimeout(data.conn); - } - - typedef CommCbMemFunT TimeoutDialer; - AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); - commSetConnTimeout(ctrl.conn, Config.Timeout.read, timeoutCall); - - typedef CommCbMemFunT Dialer; - AsyncCall::Pointer reader = JobCallback(9, 5, Dialer, this, FtpStateData::ftpReadControlReply); - comm_read(ctrl.conn, ctrl.buf + ctrl.offset, ctrl.size - ctrl.offset, reader); - } -} - -void FtpStateData::ftpReadControlReply(const CommIoCbParams &io) -{ - debugs(9, 3, "ftpReadControlReply: FD " << io.fd << ", Read " << io.size << " bytes"); - - if (io.size > 0) { - kb_incr(&(statCounter.server.all.kbytes_in), io.size); - kb_incr(&(statCounter.server.ftp.kbytes_in), io.size); - } - - if (io.flag == COMM_ERR_CLOSING) - return; - - if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { - abortTransaction("entry aborted during control reply read"); - return; - } - - assert(ctrl.offset < ctrl.size); - - if (io.flag == COMM_OK && io.size > 0) { - fd_bytes(io.fd, io.size, FD_READ); - } - - if (io.flag != COMM_OK) { - debugs(50, ignoreErrno(io.xerrno) ? 3 : DBG_IMPORTANT, - "ftpReadControlReply: read error: " << xstrerr(io.xerrno)); - - if (ignoreErrno(io.xerrno)) { - scheduleReadControlReply(0); - } else { - failed(ERR_READ_ERROR, io.xerrno); - /* failed closes ctrl.conn and frees ftpState */ - } - return; - } - - if (io.size == 0) { - if (entry->store_status == STORE_PENDING) { - failed(ERR_FTP_FAILURE, 0); - /* failed closes ctrl.conn and frees ftpState */ - return; - } - - /* XXX this may end up having to be serverComplete() .. */ - abortTransaction("zero control reply read"); - return; - } - - unsigned int len =io.size + ctrl.offset; - ctrl.offset = len; - assert(len <= ctrl.size); - handleControlReply(); -} +/* ====================================================================== */ void FtpStateData::handleControlReply() { - wordlist **W; - size_t bytes_used = 0; - wordlistDestroy(&ctrl.message); - ctrl.message = ftpParseControlReply(ctrl.buf, - ctrl.offset, &ctrl.replycode, &bytes_used); - - if (ctrl.message == NULL) { - /* didn't get complete reply yet */ - - if (ctrl.offset == ctrl.size) { - ctrl.buf = (char *)memReallocBuf(ctrl.buf, ctrl.size << 1, &ctrl.size); - } - - scheduleReadControlReply(0); - return; - } else if (ctrl.offset == bytes_used) { - /* used it all up */ - ctrl.offset = 0; - } else { - /* Got some data past the complete reply */ - assert(bytes_used < ctrl.offset); - ctrl.offset -= bytes_used; - memmove(ctrl.buf, ctrl.buf + bytes_used, ctrl.offset); - } - - /* Move the last line of the reply message to ctrl.last_reply */ - for (W = &ctrl.message; (*W)->next; W = &(*W)->next); - safe_free(ctrl.last_reply); - - ctrl.last_reply = xstrdup((*W)->key); + Ftp::ServerStateData::handleControlReply(); + if (ctrl.message == NULL) + return; // didn't get complete reply yet - wordlistDestroy(W); - - /* Copy the rest of the message to cwd_message to be printed in - * error messages + /* Copy the message except for the last line to cwd_message to be + * printed in error messages. */ - if (ctrl.message) { - for (wordlist *w = ctrl.message; w; w = w->next) { - cwd_message.append('\n'); - cwd_message.append(w->key); - } + for (wordlist *w = ctrl.message; w && w->next; w = w->next) { + cwd_message.append('\n'); + cwd_message.append(w->key); } - debugs(9, 3, HERE << "state=" << state << ", code=" << ctrl.replycode); - FTP_SM_FUNCS[state] (this); } @@ -2474,30 +1887,10 @@ ftpReadEPSV(FtpStateData* ftpState) } } - ftpState->data.port = port; - - safe_free(ftpState->data.host); - ftpState->data.host = xstrdup(fd_table[ftpState->ctrl.conn->fd].ipaddr); - - safe_free(ftpState->ctrl.last_command); - - safe_free(ftpState->ctrl.last_reply); - - ftpState->ctrl.last_command = xstrdup("Connect to server data port"); + ftpState->data.addr = fd_table[ftpState->ctrl.conn->fd].ipaddr; + ftpState->data.addr.SetPort(port); - // Generate a new data channel descriptor to be opened. - Comm::ConnectionPointer conn = new Comm::Connection; - conn->local = ftpState->ctrl.conn->local; - conn->local.SetPort(0); - conn->remote = ftpState->ctrl.conn->remote; - conn->remote.SetPort(port); - - debugs(9, 3, HERE << "connecting to " << conn->remote); - - ftpState->data.opener = commCbCall(9,3, "FtpStateData::ftpPasvCallback", CommConnectCbPtrFun(FtpStateData::ftpPasvCallback, ftpState)); - Comm::ConnOpener *cs = new Comm::ConnOpener(conn, ftpState->data.opener, Config.Timeout.connect); - cs->setHost(ftpState->data.host); - AsyncJob::Start(cs); + ftpState->connectDataChannel(); } /** \ingroup ServerProtocolFTPInternal @@ -2616,7 +2009,7 @@ ftpSendPassive(FtpStateData * ftpState) */ typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, - TimeoutDialer, ftpState, FtpStateData::ftpTimeout); + TimeoutDialer, ftpState, FtpStateData::timeout); commSetConnTimeout(ftpState->ctrl.conn, Config.Timeout.connect, timeoutCall); } @@ -2629,7 +2022,7 @@ FtpStateData::processHeadResponse() /* * On rare occasions I'm seeing the entry get aborted after - * ftpReadControlReply() and before here, probably when + * readControlReply() and before here, probably when * trying to write to the client. */ if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { @@ -2652,123 +2045,34 @@ FtpStateData::processHeadResponse() static void ftpReadPasv(FtpStateData * ftpState) { - int code = ftpState->ctrl.replycode; - int h1, h2, h3, h4; - int p1, p2; - int n; - unsigned short port; - Ip::Address ipa_remote; - char *buf; - LOCAL_ARRAY(char, ipaddr, 1024); - debugs(9, 3, HERE); - - if (code != 227) { - debugs(9, 2, "PASV not supported by remote end"); - ftpSendEPRT(ftpState); - return; - } - - /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */ - /* ANSI sez [^0-9] is undefined, it breaks on Watcom cc */ - debugs(9, 5, HERE << "scanning: " << ftpState->ctrl.last_reply); - - buf = ftpState->ctrl.last_reply + strcspn(ftpState->ctrl.last_reply, "0123456789"); - - n = sscanf(buf, "%d,%d,%d,%d,%d,%d", &h1, &h2, &h3, &h4, &p1, &p2); - - if (n != 6 || p1 < 0 || p2 < 0 || p1 > 255 || p2 > 255) { - debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << - ftpState->ctrl.conn->remote << ": " << - ftpState->ctrl.last_reply); - - ftpSendEPRT(ftpState); - return; - } - - snprintf(ipaddr, 1024, "%d.%d.%d.%d", h1, h2, h3, h4); - - ipa_remote = ipaddr; - - if ( ipa_remote.IsAnyAddr() ) { - debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << - ftpState->ctrl.conn->remote << ": " << - ftpState->ctrl.last_reply); - - ftpSendEPRT(ftpState); - return; - } - - port = ((p1 << 8) + p2); - - if (0 == port) { - debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << - ftpState->ctrl.conn->remote << ": " << - ftpState->ctrl.last_reply); - + if (ftpState->handlePasvReply()) + ftpState->connectDataChannel(); + else { ftpSendEPRT(ftpState); return; } - - if (Config.Ftp.sanitycheck) { - if (port < 1024) { - debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << - ftpState->ctrl.conn->remote << ": " << - ftpState->ctrl.last_reply); - - ftpSendEPRT(ftpState); - return; - } - } - - ftpState->data.port = port; - - safe_free(ftpState->data.host); - if (Config.Ftp.sanitycheck) - ftpState->data.host = xstrdup(fd_table[ftpState->ctrl.conn->fd].ipaddr); - else - ftpState->data.host = xstrdup(ipaddr); - - safe_free(ftpState->ctrl.last_command); - - safe_free(ftpState->ctrl.last_reply); - - ftpState->ctrl.last_command = xstrdup("Connect to server data port"); - - Comm::ConnectionPointer conn = new Comm::Connection; - conn->local = ftpState->ctrl.conn->local; - conn->local.SetPort(0); - conn->remote = ipaddr; - conn->remote.SetPort(port); - - debugs(9, 3, HERE << "connecting to " << conn->remote); - - ftpState->data.opener = commCbCall(9,3, "FtpStateData::ftpPasvCallback", CommConnectCbPtrFun(FtpStateData::ftpPasvCallback, ftpState)); - Comm::ConnOpener *cs = new Comm::ConnOpener(conn, ftpState->data.opener, Config.Timeout.connect); - cs->setHost(ftpState->data.host); - AsyncJob::Start(cs); } void -FtpStateData::ftpPasvCallback(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno, void *data) +FtpStateData::dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t err, int xerrno) { - FtpStateData *ftpState = (FtpStateData *)data; debugs(9, 3, HERE); - ftpState->data.opener = NULL; + data.opener = NULL; - if (status != COMM_OK) { + if (err != COMM_OK) { debugs(9, 2, HERE << "Failed to connect. Retrying via another method."); // ABORT on timeouts. server may be waiting on a broken TCP link. - if (status == COMM_TIMEOUT) - ftpState->writeCommand("ABOR"); + if (err == COMM_TIMEOUT) + writeCommand("ABOR"); // try another connection attempt with some other method - ftpSendPassive(ftpState); + ftpSendPassive(this); return; } - ftpState->data.opened(conn, ftpState->dataCloser()); - ftpRestOrList(ftpState); + data.opened(conn, dataCloser()); + ftpRestOrList(this); } /// \ingroup ServerProtocolFTPInternal @@ -2783,7 +2087,7 @@ ftpOpenListenSocket(FtpStateData * ftpState, int fallback) else ftpState->data.close(); } - safe_free(ftpState->data.host); + ftpState->data.addr.SetEmpty(); /* * Set up a listen socket on the same local address as the @@ -2806,7 +2110,7 @@ ftpOpenListenSocket(FtpStateData * ftpState, int fallback) temp->local.SetPort(0); } - ftpState->listenForDataChannel(temp, ftpState->entry->url()); + ftpState->listenForDataChannel(temp); } /// \ingroup ServerProtocolFTPInternal @@ -2993,9 +2297,7 @@ FtpStateData::ftpAcceptDataConnection(const CommAcceptCbParams &io) /** On COMM_OK start using the accepted data socket and discard the temporary listen socket. */ data.close(); data.opened(io.conn, dataCloser()); - static char ntoapeer[MAX_IPSTRLEN]; - io.conn->remote.NtoA(ntoapeer,sizeof(ntoapeer)); - data.host = xstrdup(ntoapeer); + data.addr = io.conn->remote; debugs(9, 3, HERE << "Connected data socket on " << io.conn << ". FD table says: " << @@ -3087,7 +2389,7 @@ void FtpStateData::readStor() } else if (code == 150) { /* When client code is 150 with no data channel, Accept data channel. */ debugs(9, 3, "ftpReadStor: accepting data channel"); - listenForDataChannel(data.conn, data.host); + listenForDataChannel(data.conn); } else { debugs(9, DBG_IMPORTANT, HERE << "Unexpected reply code "<< std::setfill('0') << std::setw(3) << code); ftpFail(this); @@ -3215,7 +2517,7 @@ ftpReadList(FtpStateData * ftpState) } else if (code == 150) { /* Accept data channel */ debugs(9, 3, HERE << "accept data channel from " << ftpState->data.conn->remote << " (" << ftpState->data.conn->local << ")"); - ftpState->listenForDataChannel(ftpState->data.conn, ftpState->data.host); + ftpState->listenForDataChannel(ftpState->data.conn); return; } else if (!ftpState->flags.tried_nlst && code > 300) { ftpSendNlst(ftpState); @@ -3256,7 +2558,7 @@ ftpReadRetr(FtpStateData * ftpState) ftpState->state = READING_DATA; } else if (code == 150) { /* Accept data channel */ - ftpState->listenForDataChannel(ftpState->data.conn, ftpState->data.host); + ftpState->listenForDataChannel(ftpState->data.conn); } else if (code >= 300) { if (!ftpState->flags.try_slash_hack) { /* Try this as a directory missing trailing slash... */ @@ -3320,17 +2622,6 @@ FtpStateData::handleRequestBodyProducerAborted() failed(ERR_READ_ERROR, 0); } -/** - * This will be called when the put write is completed - */ -void -FtpStateData::sentRequestBody(const CommIoCbParams &io) -{ - if (io.size > 0) - kb_incr(&(statCounter.server.ftp.kbytes_out), io.size); - ServerStateData::sentRequestBody(io); -} - /// \ingroup ServerProtocolFTPInternal static void ftpWriteTransferDone(FtpStateData * ftpState) @@ -3471,96 +2762,33 @@ ftpFail(FtpStateData *ftpState) /* failed() closes ctrl.conn and frees this */ } -void -FtpStateData::failed(err_type error, int xerrno) -{ - debugs(9,3,HERE << "entry-null=" << (entry?entry->isEmpty():0) << ", entry=" << entry); - if (entry->isEmpty()) - failedErrorMessage(error, xerrno); - - serverComplete(); -} - -void -FtpStateData::failedErrorMessage(err_type error, int xerrno) +Http::StatusCode +FtpStateData::failedHttpStatus(err_type &error) { - ErrorState *ftperr = NULL; - const char *command, *reply; - - /* Translate FTP errors into HTTP errors */ - switch (error) { - - case ERR_NONE: - + if (error == ERR_NONE) { switch (state) { - case SENT_USER: - case SENT_PASS: - - if (ctrl.replycode > 500) - if (password_url) - ftperr = new ErrorState(ERR_FTP_FORBIDDEN, Http::scForbidden, fwd->request); - else - ftperr = new ErrorState(ERR_FTP_FORBIDDEN, Http::scUnauthorized, fwd->request); - - else if (ctrl.replycode == 421) - ftperr = new ErrorState(ERR_FTP_UNAVAILABLE, Http::scServiceUnavailable, fwd->request); - + if (ctrl.replycode > 500) { + error = ERR_FTP_FORBIDDEN; + return password_url ? Http::scForbidden : Http::scUnauthorized; + } else if (ctrl.replycode == 421) { + error = ERR_FTP_UNAVAILABLE; + return Http::scServiceUnavailable; + } break; - case SENT_CWD: - case SENT_RETR: - if (ctrl.replycode == 550) - ftperr = new ErrorState(ERR_FTP_NOT_FOUND, Http::scNotFound, fwd->request); - + if (ctrl.replycode == 550) { + error = ERR_FTP_NOT_FOUND; + return Http::scNotFound; + } break; - default: break; } - - break; - - case ERR_READ_TIMEOUT: - ftperr = new ErrorState(error, Http::scGateway_Timeout, fwd->request); - break; - - default: - ftperr = new ErrorState(error, Http::scBadGateway, fwd->request); - break; } - - if (ftperr == NULL) - ftperr = new ErrorState(ERR_FTP_FAILURE, Http::scBadGateway, fwd->request); - - ftperr->xerrno = xerrno; - - ftperr->ftp.server_msg = ctrl.message; - ctrl.message = NULL; - - if (old_request) - command = old_request; - else - command = ctrl.last_command; - - if (command && strncmp(command, "PASS", 4) == 0) - command = "PASS "; - - if (old_reply) - reply = old_reply; - else - reply = ctrl.last_reply; - - if (command) - ftperr->ftp.request = xstrdup(command); - - if (reply) - ftperr->ftp.reply = xstrdup(reply); - - entry->replaceHttpReply( ftperr->BuildHttpReply() ); - delete ftperr; + return Ftp::ServerStateData::failedHttpStatus(error); } /// \ingroup ServerProtocolFTPInternal @@ -3779,21 +3007,6 @@ FtpStateData::writeReplyBody(const char *dataToWrite, size_t dataLength) addVirginReplyBody(dataToWrite, dataLength); } -/** - * called after we wrote the last byte of the request body - */ -void -FtpStateData::doneSendingRequestBody() -{ - ServerStateData::doneSendingRequestBody(); - debugs(9,3, HERE); - dataComplete(); - /* NP: RFC 959 3.3. DATA CONNECTION MANAGEMENT - * if transfer type is 'stream' call dataComplete() - * otherwise leave open. (reschedule control channel read?) - */ -} - /** * A hack to ensure we do not double-complete on the forward entry. * @@ -3814,38 +3027,6 @@ FtpStateData::completeForwarding() ServerStateData::completeForwarding(); } -/** - * Close the FTP server connection(s). Used by serverComplete(). - */ -void -FtpStateData::closeServer() -{ - if (Comm::IsConnOpen(ctrl.conn)) { - debugs(9,3, HERE << "closing FTP server FD " << ctrl.conn->fd << ", this " << this); - fwd->unregister(ctrl.conn); - ctrl.close(); - } - - if (Comm::IsConnOpen(data.conn)) { - debugs(9,3, HERE << "closing FTP data FD " << data.conn->fd << ", this " << this); - data.close(); - } - - debugs(9,3, HERE << "FTP ctrl and data connections closed. this " << this); -} - -/** - * Did we close all FTP server connection(s)? - * - \retval true Both server control and data channels are closed. And not waiting for a new data connection to open. - \retval false Either control channel or data is still active. - */ -bool -FtpStateData::doneWithServer() const -{ - return !Comm::IsConnOpen(ctrl.conn) && !Comm::IsConnOpen(data.conn); -} - /** * Have we lost the FTP server control channel? * @@ -3867,65 +3048,3 @@ FtpStateData::haveControlChannel(const char *caller_name) const return true; } - -/** - * Quickly abort the transaction - * - \todo destruction should be sufficient as the destructor should cleanup, - * including canceling close handlers - */ -void -FtpStateData::abortTransaction(const char *reason) -{ - debugs(9, 3, HERE << "aborting transaction for " << reason << - "; FD " << (ctrl.conn!=NULL?ctrl.conn->fd:-1) << ", Data FD " << (data.conn!=NULL?data.conn->fd:-1) << ", this " << this); - if (Comm::IsConnOpen(ctrl.conn)) { - ctrl.conn->close(); - return; - } - - fwd->handleUnregisteredServerEnd(); - mustStop("FtpStateData::abortTransaction"); -} - -/// creates a data channel Comm close callback -AsyncCall::Pointer -FtpStateData::dataCloser() -{ - typedef CommCbMemFunT Dialer; - return JobCallback(9, 5, Dialer, this, FtpStateData::dataClosed); -} - -/// configures the channel with a descriptor and registers a close handler -void -FtpChannel::opened(const Comm::ConnectionPointer &newConn, const AsyncCall::Pointer &aCloser) -{ - assert(!Comm::IsConnOpen(conn)); - assert(closer == NULL); - - assert(Comm::IsConnOpen(newConn)); - assert(aCloser != NULL); - - conn = newConn; - closer = aCloser; - comm_add_close_handler(conn->fd, closer); -} - -/// planned close: removes the close handler and calls comm_close -void -FtpChannel::close() -{ - // channels with active listeners will be closed when the listener handler dies. - if (Comm::IsConnOpen(conn)) { - comm_remove_close_handler(conn->fd, closer); - conn->close(); // we do not expect to be called back - } - clear(); -} - -void -FtpChannel::clear() -{ - conn = NULL; - closer = NULL; -} diff --git a/src/ipc/FdNotes.cc b/src/ipc/FdNotes.cc index d0e993b05d..a85c65b8df 100644 --- a/src/ipc/FdNotes.cc +++ b/src/ipc/FdNotes.cc @@ -14,6 +14,7 @@ Ipc::FdNote(int fdNoteId) "None", // fdnNone "HTTP Socket", // fdnHttpSocket "HTTPS Socket", // fdnHttpsSocket + "FTP Socket", // fdnFtpSocket #if SQUID_SNMP "Incoming SNMP Socket", // fdnInSnmpSocket "Outgoing SNMP Socket", // fdnOutSnmpSocket diff --git a/src/ipc/FdNotes.h b/src/ipc/FdNotes.h index 1e5730c3c0..8367a4f726 100644 --- a/src/ipc/FdNotes.h +++ b/src/ipc/FdNotes.h @@ -12,7 +12,7 @@ namespace Ipc /// We cannot send char* FD notes to other processes. Pass int IDs and convert. /// fd_note() label ID -typedef enum { fdnNone, fdnHttpSocket, fdnHttpsSocket, +typedef enum { fdnNone, fdnHttpSocket, fdnHttpsSocket, fdnFtpSocket, #if SQUID_SNMP fdnInSnmpSocket, fdnOutSnmpSocket, #endif diff --git a/src/main.cc b/src/main.cc index 3f04daa5cf..d8082f680a 100644 --- a/src/main.cc +++ b/src/main.cc @@ -735,7 +735,7 @@ serverConnectionsClose(void) #endif } if (IamWorkerProcess()) { - clientHttpConnectionsClose(); + clientConnectionsClose(); icpConnectionShutdown(); #if USE_HTCP htcpSocketShutdown(); diff --git a/src/tools.cc b/src/tools.cc index 87da3f3561..a413f66916 100644 --- a/src/tools.cc +++ b/src/tools.cc @@ -107,8 +107,8 @@ releaseServerSockets(void) { // Release the main ports as early as possible - // clear both http_port and https_port lists. - clientHttpConnectionsClose(); + // clear http_port, https_port and ftp_port lists + clientConnectionsClose(); // clear icp_port's icpClosePorts();