--- /dev/null
- return error == ERR_READ_TIMEOUT ? Http::scGateway_Timeout :
+/*
+ * DEBUG: section 09 File Transfer Protocol (FTP)
+ *
+ */
+
+#include "squid.h"
+
++#include "acl/FilledChecklist.h"
+#include "FtpServer.h"
+#include "Mem.h"
+#include "SquidConfig.h"
+#include "StatCounters.h"
+#include "client_side.h"
+#include "comm/ConnOpener.h"
+#include "comm/TcpAcceptor.h"
+#include "comm/Write.h"
+#include "errorpage.h"
+#include "fd.h"
+#include "ip/tools.h"
+#include "SquidString.h"
+#include "tools.h"
+#include "wordlist.h"
+#include <set>
+
+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
+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::forget()
+{
+ if (Comm::IsConnOpen(conn))
+ comm_remove_close_handler(conn->fd, closer);
+ clear();
+}
+
+void
+FtpChannel::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<char *>(memAllocBuf(4096, &ctrl.size));
+ ctrl.offset = 0;
+
+ typedef CommCbMemFunT<ServerStateData, CommCloseCbParams> Dialer;
+ const AsyncCall::Pointer closer = JobCallback(9, 5, Dialer, this,
+ ServerStateData::ctrlClosed);
+ ctrl.opened(fwdState->serverConnection(), closer);
+}
+
+void
+ServerStateData::DataChannel::addr(const Ip::Address &import)
+{
+ static char addrBuf[MAX_IPSTRLEN];
+ import.toStr(addrBuf, sizeof(addrBuf));
+ xfree(host);
+ host = xstrdup(addrBuf);
+ port = import.port();
+}
+
+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);
+}
+
+void
+ServerStateData::initReadBuf()
+{
+ if (data.readBuf == NULL) {
+ data.readBuf = new MemBuf;
+ data.readBuf->init(4096, SQUID_TCP_SO_RCVBUF);
+ }
+}
+
+/**
+ * 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);
+
+ 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 <yourpassword>";
+
+ 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);
+
+ fwd->request->detailError(error, xerrno);
+ fwd->fail(ftperr);
+
+ closeServer(); // we failed, so no serverComplete()
+}
+
+Http::StatusCode
+ServerStateData::failedHttpStatus(err_type &error)
+{
+ if (error == ERR_NONE)
+ error = ERR_FTP_FAILURE;
- case Ftp::ServerStateData::SENT_EPSV_2: /* EPSV IPv6 failed. Try EPSV IPv4 */
++ return error == ERR_READ_TIMEOUT ? Http::scGatewayTimeout :
+ 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<ServerStateData, CommTimeoutCbParams> TimeoutDialer;
+ AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, ServerStateData::timeout);
+ commSetConnTimeout(ctrl.conn, Config.Timeout.read, timeoutCall);
+
+ typedef CommCbMemFunT<ServerStateData, CommIoCbParams> 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);
+
+ if (!parseControlReply(bytes_used)) {
+ /* 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;
+ }
+
+ assert(ctrl.message); // the entire FTP server response, line by line
+ assert(ctrl.replycode >= 0); // FTP status code (from the last line)
+ assert(ctrl.last_reply); // FTP reason (from the last line)
+
+ 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);
+ }
+
+ debugs(9, 3, HERE << "state=" << state << ", code=" << ctrl.replycode);
+}
+
+bool
+ServerStateData::handlePasvReply(Ip::Address &srvAddr)
+{
+ int code = ctrl.replycode;
+ char *buf;
+ 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");
+
+ const char *forceIp = Config.Ftp.sanitycheck ?
+ fd_table[ctrl.conn->fd].ipaddr : NULL;
+ if (!Ftp::ParseIpPort(buf, forceIp, srvAddr)) {
+ debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " <<
+ ctrl.conn->remote << ": " << ctrl.last_reply);
+ return false;
+ }
+
+ data.addr(srvAddr);
+
+ return true;
+}
+
+bool
+ServerStateData::handleEpsvReply(Ip::Address &remoteAddr)
+{
+ int code = ctrl.replycode;
+ char *buf;
+ debugs(9, 3, HERE);
+
+ if (code != 229 && code != 522) {
+ if (code == 200) {
+ /* handle broken servers (RFC 2428 says OK code for EPSV MUST be 229 not 200) */
+ /* vsftpd for one send '200 EPSV ALL ok.' without even port info.
+ * Its okay to re-send EPSV 1/2 but nothing else. */
+ debugs(9, DBG_IMPORTANT, "Broken FTP Server at " << ctrl.conn->remote << ". Wrong accept code for EPSV");
+ } else {
+ debugs(9, 2, "EPSV not supported by remote end");
+ }
+ return sendPassive();
+ }
+
+ if (code == 522) {
+ /* server response with list of supported methods */
+ /* 522 Network protocol not supported, use (1) */
+ /* 522 Network protocol not supported, use (1,2) */
+ /* 522 Network protocol not supported, use (2) */
+ /* TODO: handle the (1,2) case. We might get it back after EPSV ALL
+ * which means close data + control without self-destructing and re-open from scratch. */
+ debugs(9, 5, HERE << "scanning: " << ctrl.last_reply);
+ buf = ctrl.last_reply;
+ while (buf != NULL && *buf != '\0' && *buf != '\n' && *buf != '(')
+ ++buf;
+ if (buf != NULL && *buf == '\n')
+ ++buf;
+
+ if (buf == NULL || *buf == '\0') {
+ /* handle broken server (RFC 2428 says MUST specify supported protocols in 522) */
+ debugs(9, DBG_IMPORTANT, "Broken FTP Server at " << ctrl.conn->remote << ". 522 error missing protocol negotiation hints");
+ return sendPassive();
+ } else if (strcmp(buf, "(1)") == 0) {
+ state = SENT_EPSV_2; /* simulate having sent and failed EPSV 2 */
+ return sendPassive();
+ } else if (strcmp(buf, "(2)") == 0) {
+ if (Ip::EnableIpv6) {
+ /* If server only supports EPSV 2 and we have already tried that. Go straight to EPRT */
+ if (state == SENT_EPSV_2) {
+ return sendEprt();
+ } else {
+ /* or try the next Passive mode down the chain. */
+ return sendPassive();
+ }
+ } else {
+ /* Server only accept EPSV in IPv6 traffic. */
+ state = SENT_EPSV_1; /* simulate having sent and failed EPSV 1 */
+ return sendPassive();
+ }
+ } else {
+ /* handle broken server (RFC 2428 says MUST specify supported protocols in 522) */
+ debugs(9, DBG_IMPORTANT, "WARNING: Server at " << ctrl.conn->remote << " sent unknown protocol negotiation hint: " << buf);
+ return sendPassive();
+ }
+ failed(ERR_FTP_FAILURE, 0);
+ return false;
+ }
+
+ /* 229 Entering Extended Passive Mode (|||port|) */
+ /* ANSI sez [^0-9] is undefined, it breaks on Watcom cc */
+ debugs(9, 5, "scanning: " << ctrl.last_reply);
+
+ buf = ctrl.last_reply + strcspn(ctrl.last_reply, "(");
+
+ char h1, h2, h3, h4;
+ unsigned short port;
+ int n = sscanf(buf, "(%c%c%c%hu%c)", &h1, &h2, &h3, &port, &h4);
+
+ if (n < 4 || h1 != h2 || h1 != h3 || h1 != h4) {
+ debugs(9, DBG_IMPORTANT, "Invalid EPSV reply from " <<
+ ctrl.conn->remote << ": " <<
+ ctrl.last_reply);
+
+ return sendPassive();
+ }
+
+ if (0 == port) {
+ debugs(9, DBG_IMPORTANT, "Unsafe EPSV reply from " <<
+ ctrl.conn->remote << ": " <<
+ ctrl.last_reply);
+
+ return sendPassive();
+ }
+
+ if (Config.Ftp.sanitycheck) {
+ if (port < 1024) {
+ debugs(9, DBG_IMPORTANT, "Unsafe EPSV reply from " <<
+ ctrl.conn->remote << ": " <<
+ ctrl.last_reply);
+
+ return sendPassive();
+ }
+ }
+
+ remoteAddr = ctrl.conn->remote;
+ remoteAddr.port(port);
+ data.addr(remoteAddr);
+ return true;
+}
+
+// The server-side EPRT and PORT commands are not yet implemented.
+// The ServerStateData::sendEprt() will fail because of the unimplemented
+// openListenSocket() or sendPort() methods
+bool
+ServerStateData::sendEprt()
+{
+ if (!Config.Ftp.eprt) {
+ /* Disabled. Switch immediately to attempting old PORT command. */
+ debugs(9, 3, "EPRT disabled by local administrator");
+ return sendPort();
+ }
+
+ debugs(9, 3, HERE);
+
+ if (!openListenSocket()) {
+ failed(ERR_FTP_FAILURE, 0);
+ return false;
+ }
+
+ debugs(9, 3, "Listening for FTP data connection with FD " << data.conn);
+ if (!Comm::IsConnOpen(data.conn)) {
+ /* XXX Need to set error message */
+ failed(ERR_FTP_FAILURE, 0);
+ return false;
+ }
+
+ static MemBuf mb;
+ mb.reset();
+ char buf[MAX_IPSTRLEN];
+ /* RFC 2428 defines EPRT as IPv6 equivalent to IPv4 PORT command. */
+ /* Which can be used by EITHER protocol. */
+ debugs(9, 3, "Listening for FTP data connection on port" << comm_local_port(data.conn->fd) << " or port?" << data.conn->local.port());
+ mb.Printf("EPRT |%d|%s|%d|%s",
+ ( data.conn->local.isIPv6() ? 2 : 1 ),
+ data.conn->local.toStr(buf,MAX_IPSTRLEN),
+ comm_local_port(data.conn->fd), Ftp::crlf );
+
+ state = SENT_EPRT;
+ writeCommand(mb.content());
+ return true;
+}
+
+bool
+ServerStateData::sendPort()
+{
+ failed(ERR_FTP_FAILURE, 0);
+ return false;
+}
+
+bool
+ServerStateData::sendPassive()
+{
+ debugs(9, 3, HERE);
+
+ /** \par
+ * Checks for EPSV ALL special conditions:
+ * If enabled to be sent, squid MUST NOT request any other connect methods.
+ * If 'ALL' is sent and fails the entire FTP Session fails.
+ * NP: By my reading exact EPSV protocols maybe attempted, but only EPSV method. */
+ if (Config.Ftp.epsv_all && state == SENT_EPSV_1 ) {
+ // We are here because the last "EPSV 1" failed, but because of epsv_all
+ // no other method allowed.
+ debugs(9, DBG_IMPORTANT, "FTP does not allow PASV method after 'EPSV ALL' has been sent.");
+ failed(ERR_FTP_FAILURE, 0);
+ return false;
+ }
+
+
+ /// Closes any old FTP-Data connection which may exist. */
+ data.close();
+
+ /** \par
+ * Checks for previous EPSV/PASV failures on this server/session.
+ * Diverts to EPRT immediately if they are not working. */
+ if (!Config.Ftp.passive || state == SENT_PASV) {
+ sendEprt();
+ return true;
+ }
+
+ static MemBuf mb;
+ mb.reset();
+ /** \par
+ * Send EPSV (ALL,2,1) or PASV on the control channel.
+ *
+ * - EPSV ALL is used if enabled.
+ * - EPSV 2 is used if ALL is disabled and IPv6 is available and ctrl channel is IPv6.
+ * - EPSV 1 is used if EPSV 2 (IPv6) fails or is not available or ctrl channel is IPv4.
+ * - PASV is used if EPSV 1 fails.
+ */
+ switch (state) {
+ case SENT_EPSV_ALL: /* EPSV ALL resulted in a bad response. Try ther EPSV methods. */
+ if (ctrl.conn->local.isIPv6()) {
+ debugs(9, 5, HERE << "FTP Channel is IPv6 (" << ctrl.conn->remote << ") attempting EPSV 2 after EPSV ALL has failed.");
+ mb.Printf("EPSV 2%s", Ftp::crlf);
+ state = SENT_EPSV_2;
+ break;
+ }
+ // else fall through to skip EPSV 2
+
- default:
- if (!Config.Ftp.epsv) {
++ case SENT_EPSV_2: /* EPSV IPv6 failed. Try EPSV IPv4 */
+ if (ctrl.conn->local.isIPv4()) {
+ debugs(9, 5, HERE << "FTP Channel is IPv4 (" << ctrl.conn->remote << ") attempting EPSV 1 after EPSV ALL has failed.");
+ mb.Printf("EPSV 1%s", Ftp::crlf);
+ state = SENT_EPSV_1;
+ break;
+ } else if (Config.Ftp.epsv_all) {
+ debugs(9, DBG_IMPORTANT, "FTP does not allow PASV method after 'EPSV ALL' has been sent.");
+ failed(ERR_FTP_FAILURE, 0);
+ return false;
+ }
+ // else fall through to skip EPSV 1
+
+ case SENT_EPSV_1: /* EPSV options exhausted. Try PASV now. */
+ debugs(9, 5, HERE << "FTP Channel (" << ctrl.conn->remote << ") rejects EPSV connection attempts. Trying PASV instead.");
+ mb.Printf("PASV%s", Ftp::crlf);
+ state = SENT_PASV;
+ break;
+
-
- return true;
-
++ default: {
++ bool doEpsv = true;
++ if (Config.accessList.ftp_epsv) {
++ ACLFilledChecklist checklist(Config.accessList.ftp_epsv, fwd->request, NULL);
++ doEpsv = (checklist.fastCheck() == ACCESS_ALLOWED);
++ }
++ if (!doEpsv) {
+ debugs(9, 5, HERE << "EPSV support manually disabled. Sending PASV for FTP Channel (" << ctrl.conn->remote <<")");
+ mb.Printf("PASV%s", Ftp::crlf);
+ state = SENT_PASV;
+ } else if (Config.Ftp.epsv_all) {
+ debugs(9, 5, HERE << "EPSV ALL manually enabled. Attempting with FTP Channel (" << ctrl.conn->remote <<")");
+ mb.Printf("EPSV ALL%s", Ftp::crlf);
+ state = SENT_EPSV_ALL;
+ } else {
+ if (ctrl.conn->local.isIPv6()) {
+ debugs(9, 5, HERE << "FTP Channel (" << ctrl.conn->remote << "). Sending default EPSV 2");
+ mb.Printf("EPSV 2%s", Ftp::crlf);
+ state = SENT_EPSV_2;
+ }
+ if (ctrl.conn->local.isIPv4()) {
+ debugs(9, 5, HERE << "Channel (" << ctrl.conn->remote <<"). Sending default EPSV 1");
+ mb.Printf("EPSV 1%s", Ftp::crlf);
+ state = SENT_EPSV_1;
+ }
+ }
+ break;
+ }
++ }
+
+ if (ctrl.message)
+ wordlistDestroy(&ctrl.message);
+ ctrl.message = NULL; //No message to return to client.
+ ctrl.offset = 0; //reset readed response, to make room read the next response
+
+ writeCommand(mb.content());
- /*
+
+ /*
+ * ugly hack for ftp servers like ftp.netscape.com that sometimes
+ * dont acknowledge PASV commands. Use connect timeout to be faster then read timeout (minutes).
+ */
- TimeoutDialer, ftpState, FtpStateData::timeout);
- commSetConnTimeout(ftpState->ctrl.conn, Config.Timeout.connect, timeoutCall);
- return true;
++ /* XXX: resurrect or remove
+ typedef CommCbMemFunT<FtpStateData, CommTimeoutCbParams> TimeoutDialer;
+ AsyncCall::Pointer timeoutCall = JobCallback(9, 5,
++ TimeoutDialer, this, FtpStateData::timeout);
++ commSetConnTimeout(ctrl.conn, Config.Timeout.connect, timeoutCall);
+ */
++
++ return true;
+}
+
+
+void
+ServerStateData::connectDataChannel()
+{
+ safe_free(ctrl.last_command);
+
+ safe_free(ctrl.last_reply);
+
+ ctrl.last_command = xstrdup("Connect to server data port");
+
+ // Generate a new data channel descriptor to be opened.
+ Comm::ConnectionPointer conn = new Comm::Connection;
+ conn->local = ctrl.conn->local;
+ conn->local.port(0);
+ conn->remote = data.host;
+ conn->remote.port(data.port);
+
+ 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);
+ cs->setHost(data.host);
+ AsyncJob::Start(cs);
+}
+
+void
+ServerStateData::dataChannelConnected(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno, void *data)
+{
+ ServerStateData *ftpState = static_cast<ServerStateData *>(data);
+ ftpState->dataChannelConnected(conn, status, xerrno);
+}
+
+bool
+ServerStateData::openListenSocket()
+{
+ return false;
+}
+
+/// creates a data channel Comm close callback
+AsyncCall::Pointer
+ServerStateData::dataCloser()
+{
+ typedef CommCbMemFunT<ServerStateData, CommCloseCbParams> 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<ServerStateData, CommIoCbParams> 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;
+
+ initReadBuf();
+
+ 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<ServerStateData, CommTimeoutCbParams> 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<ServerStateData, CommIoCbParams> 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<ServerStateData, CommTimeoutCbParams> 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<ServerStateData, CommTimeoutCbParams> 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?)
+ */
+}
+
+/// Parses FTP server control response into ctrl structure fields,
+/// setting bytesUsed and returning true on success.
+bool
+ServerStateData::parseControlReply(size_t &bytesUsed)
+{
+ char *s;
+ char *sbuf;
+ char *end;
+ int usable;
+ int complete = 0;
+ wordlist *head = NULL;
+ wordlist *list;
+ wordlist **tail = &head;
+ size_t linelen;
+ debugs(9, 3, HERE);
+ /*
+ * We need a NULL-terminated buffer for scanning, ick
+ */
+ const size_t len = ctrl.offset;
+ sbuf = (char *)xmalloc(len + 1);
+ xstrncpy(sbuf, ctrl.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 false;
+ }
+
+ 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) == ' ');
+
+ list = new wordlist();
+
+ list->key = (char *)xmalloc(linelen);
+
+ xstrncpy(list->key, s, linelen);
+
+ /* trace the FTP communication chat at level 2 */
+ debugs(9, 2, "ftp>> " << list->key);
+
+ if (complete) {
+ // use list->key for last_reply because s contains the new line
+ ctrl.last_reply = xstrdup(list->key + 4);
+ ctrl.replycode = atoi(list->key);
+ }
+
+ *tail = list;
+
+ tail = &list->next;
+ }
+
+ bytesUsed = static_cast<size_t>(s - sbuf);
+ safe_free(sbuf);
+
+ if (!complete) {
+ wordlistDestroy(&head);
+ return false;
+ }
+
+ ctrl.message = head;
+ assert(ctrl.replycode >= 0);
+ assert(ctrl.last_reply);
+ assert(ctrl.message);
+ return true;
+}
+
+}; // namespace Ftp
+
+
+bool
+Ftp::ParseIpPort(const char *buf, const char *forceIp, Ip::Address &addr)
+{
+ int h1, h2, h3, h4;
+ int p1, p2;
+ const int 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)
+ return false;
+
+ if (forceIp) {
+ addr = forceIp; // but the above code still validates the IP we got
+ } else {
+ static char ipBuf[1024];
+ snprintf(ipBuf, sizeof(ipBuf), "%d.%d.%d.%d", h1, h2, h3, h4);
+ addr = ipBuf;
+
+ if (addr.isAnyAddr())
+ return false;
+ }
+
+ const int port = ((p1 << 8) + p2);
+
+ if (port <= 0)
+ return false;
+
+ if (Config.Ftp.sanitycheck && port < 1024)
+ return false;
+
+ addr.port(port);
+ return true;
+}
+
+bool
+Ftp::ParseProtoIpPort(const char *buf, Ip::Address &addr)
+{
+
+ const char delim = *buf;
+ const char *s = buf + 1;
+ const char *e = s;
+ const int proto = strtol(s, const_cast<char**>(&e), 10);
+ if ((proto != 1 && proto != 2) || *e != delim)
+ return false;
+
+ s = e + 1;
+ e = strchr(s, delim);
+ char ip[MAX_IPSTRLEN];
+ if (static_cast<size_t>(e - s) >= sizeof(ip))
+ return false;
+ strncpy(ip, s, e - s);
+ ip[e - s] = '\0';
+ addr = ip;
+
+ if (addr.isAnyAddr())
+ return false;
+
+ if ((proto == 2) != addr.isIPv6()) // proto ID mismatches address version
+ return false;
+
+ s = e + 1; // skip port delimiter
+ const int port = strtol(s, const_cast<char**>(&e), 10);
+ if (port < 0 || *e != '|')
+ return false;
+
+ if (Config.Ftp.sanitycheck && port < 1024)
+ return false;
+
+ addr.port(port);
+ return true;
+}
+
+const char *
+Ftp::unescapeDoubleQuoted(const char *quotedPath)
+{
+ static MemBuf path;
+ path.reset();
+ const char *s = quotedPath;
+ if (*s == '"') {
+ ++s;
+ bool parseDone = false;
+ while (!parseDone) {
+ if (const char *e = strchr(s, '"')) {
+ path.append(s, e - s);
+ s = e + 1;
+ if (*s == '"') {
+ path.append(s, 1);
+ ++s;
+ } else
+ parseDone = true;
+ } else { //parse error
+ parseDone = true;
+ path.reset();
+ }
+ }
+ }
+ return path.content();
+}
+
+bool
+Ftp::hasPathParameter(const String &cmd)
+{
+ static const char *pathCommandsStr[]= {"CWD","SMNT", "RETR", "STOR", "APPE",
+ "RNFR", "RNTO", "DELE", "RMD", "MKD",
+ "LIST", "NLST", "STAT", "MLSD", "MLST"};
+ static const std::set<String> pathCommands(pathCommandsStr, pathCommandsStr + sizeof(pathCommandsStr)/sizeof(pathCommandsStr[0]));
+ return pathCommands.find(cmd) != pathCommands.end();
+}
#if USE_DELAY_POOLS
#include "ClientInfo.h"
#endif
- #if USE_SSL
- #include "ssl/ProxyCerts.h"
+ #if USE_OPENSSL
#include "ssl/context_storage.h"
+ #include "ssl/gadgets.h"
#include "ssl/helper.h"
+ #include "ssl/ProxyCerts.h"
#include "ssl/ServerBump.h"
#include "ssl/support.h"
- #include "ssl/gadgets.h"
#endif
#if USE_SSL_CRTD
- #include "ssl/crtd_message.h"
#include "ssl/certificate_db.h"
+ #include "ssl/crtd_message.h"
#endif
- #if HAVE_LIMITS_H
- #include <limits.h>
- #endif
- #if HAVE_MATH_H
- #include <math.h>
- #endif
- #if HAVE_LIMITS
+ #include <climits>
+ #include <cmath>
#include <limits>
- #endif
+#include <set>
#if LINGERING_CLOSE
#define comm_close comm_lingering_close
static IOCB clientWriteComplete;
static IOCB clientWriteBodyComplete;
static IOACB httpAccept;
- #if USE_SSL
+ #if USE_OPENSSL
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 *);
char *skipLeadingSpace(char *aString);
static void connNoteUseOfBuffer(ConnStateData* conn, size_t byteCount);
++static void FtpChangeState(ConnStateData *connState, const ConnStateData::FtpState newState, const char *reason);
+static IOACB FtpAcceptDataConnection;
+static void FtpCloseDataConnection(ConnStateData *conn);
+static ClientSocketContext *FtpParseRequest(ConnStateData *connState, HttpRequestMethod *method_p, Http::ProtocolVersion *http_ver);
+static bool FtpHandleUserRequest(ConnStateData *connState, const String &cmd, String ¶ms);
+static CNCB FtpHandleConnectDone;
+
+static void FtpHandleReply(ClientSocketContext *context, HttpReply *reply, StoreIOBuffer data);
+typedef void FtpReplyHandler(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data);
+static FtpReplyHandler FtpHandleFeatReply;
+static FtpReplyHandler FtpHandlePasvReply;
+static FtpReplyHandler FtpHandlePortReply;
+static FtpReplyHandler FtpHandleErrorReply;
+static FtpReplyHandler FtpHandleDataReply;
+static FtpReplyHandler FtpHandleUploadReply;
+static FtpReplyHandler FtpHandleEprtReply;
+static FtpReplyHandler FtpHandleEpsvReply;
+
+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 FtpWriteErrorReply(ClientSocketContext *context, const HttpReply *reply, const int status);
+
+static void FtpPrintReply(MemBuf &mb, const HttpReply *reply, const char *const prefix = "");
+static IOCB FtpWroteEarlyReply;
+static IOCB FtpWroteReply;
+static IOCB FtpWroteReplyData;
+
+typedef bool FtpRequestHandler(ClientSocketContext *context, String &cmd, String ¶ms);
+static FtpRequestHandler FtpHandleRequest;
+static FtpRequestHandler FtpHandleFeatRequest;
+static FtpRequestHandler FtpHandlePasvRequest;
+static FtpRequestHandler FtpHandlePortRequest;
+static FtpRequestHandler FtpHandleDataRequest;
+static FtpRequestHandler FtpHandleUploadRequest;
+static FtpRequestHandler FtpHandleEprtRequest;
+static FtpRequestHandler FtpHandleEpsvRequest;
+static FtpRequestHandler FtpHandleCwdRequest;
+static FtpRequestHandler FtpHandlePassRequest;
+static FtpRequestHandler FtpHandleCdupRequest;
+
+static bool FtpCheckDataConnPre(ClientSocketContext *context);
+static bool FtpCheckDataConnPost(ClientSocketContext *context);
+static void FtpSetDataCommand(ClientSocketContext *context);
+static void FtpSetReply(ClientSocketContext *context, const int code, const char *msg);
+static bool FtpSupportedCommand(const String &name);
+
+
clientStreamNode *
ClientSocketContext::getTail() const
{
typedef CommCbMemFunT<ConnStateData, CommIoCbParams> Dialer;
reader = JobCallback(33, 5, Dialer, this, ConnStateData::clientReadRequest);
- comm_read(clientConnection, in.addressToReadInto(), getAvailableBufferLength(), reader);
+ comm_read(clientConnection, in.buf, reader);
}
+void
+ConnStateData::readSomeFtpData()
+{
+ if (ftp.reader != NULL)
+ return;
+
+ const size_t availSpace = sizeof(ftp.uploadBuf) - ftp.uploadAvailSize;
+ if (availSpace <= 0)
+ return;
+
+ debugs(33, 4, HERE << ftp.dataConn << ": reading FTP data...");
+
+ typedef CommCbMemFunT<ConnStateData, CommIoCbParams> Dialer;
+ ftp.reader = JobCallback(33, 5, Dialer, this,
+ ConnStateData::clientReadFtpData);
+ comm_read(ftp.dataConn, ftp.uploadBuf + ftp.uploadAvailSize, availSpace,
+ ftp.reader);
+}
+
void
ClientSocketContext::removeFromConnectionList(ConnStateData * conn)
{
debugs(33, 9, "clientLogRequest: http.code='" << al->http.code << "'");
- if (loggingEntry() && loggingEntry()->mem_obj)
+ if (loggingEntry() && loggingEntry()->mem_obj && loggingEntry()->objectLen() >= 0)
- al->cache.objectSize = loggingEntry()->contentLen();
+ al->cache.objectSize = loggingEntry()->contentLen(); // payload duplicate ?? with or without TE ?
- al->cache.caddr.setNoAddr();
-
- if (getConn() != NULL) {
- al->cache.caddr = getConn()->log_addr;
- al->cache.port = cbdataReference(getConn()->port);
- }
-
- al->cache.requestSize = req_sz;
- al->cache.requestHeadersSize = req_sz;
-
- al->cache.replySize = out.size;
- al->cache.replyHeadersSize = out.headers_sz;
+ al->http.clientRequestSz.header = req_sz;
+ al->http.clientReplySz.header = out.headers_sz;
+ // XXX: calculate without payload encoding or headers !!
+ al->http.clientReplySz.payloadData = out.size - out.headers_sz; // pretend its all un-encoded data for now.
al->cache.highOffset = out.offset;
if (concurrentRequestQueueFilled())
break;
- /* Should not be needed anymore */
- /* Terminate the string */
- in.buf[in.notYetUsed] = '\0';
- /* Begin the parsing */
- PROF_start(parseHttpRequest);
- HttpParserInit(&parser_, in.buf.c_str(), in.buf.length());
--
- Http::ProtocolVersion http_ver;
- /* Process request */
+ ClientSocketContext *context = NULL;
+ Http::ProtocolVersion http_ver;
- ClientSocketContext *context = parseHttpRequest(this, &parser_, &method, &http_ver);
- PROF_stop(parseHttpRequest);
+ if (!isFtp) {
+ /* Begin the parsing */
+ PROF_start(parseHttpRequest);
- HttpParserInit(&parser_, in.buf, in.notYetUsed);
++ HttpParserInit(&parser_, in.buf.c_str(), in.buf.length());
+
+ /* 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) {
io.conn->close();
}
-ConnStateData::ConnStateData(const MasterXaction::Pointer &xact) :
+ConnStateData::ConnStateData(const MasterXaction::Pointer &xact):
AsyncJob("ConnStateData"),
- isFtp(strcmp(xact->squidPort->protocol, "ftp") == 0), // TODO: convert into a method?
- #if USE_SSL
++ isFtp(xact->squidPort->transport.protocol == AnyP::PROTO_FTP), // TODO: convert into a method?
+ #if USE_OPENSSL
sslBumpMode(Ssl::bumpEnd),
switchedToHttps_(false),
sslServerBump(NULL),
clientOpenListenSockets(void)
{
clientHttpConnectionsOpen();
- #if USE_SSL
+ #if USE_OPENSSL
clientHttpsConnectionsOpen();
#endif
+ clientFtpConnectionsOpen();
if (NHttpSockets < 1)
- fatal("No HTTP or HTTPS ports configured");
+ fatal("No HTTP, HTTPS or FTP ports configured");
}
void
assert(pinning.serverConnection == io.conn);
pinning.closeHandler = NULL; // Comm unregisters handlers before calling
const bool sawZeroReply = pinning.zeroReply; // reset when unpinning
- unpinConnection();
+ unpinConnection(false);
- if (sawZeroReply) {
- 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.");
++ // if the server control connection is gone, reset state to login again
++ // TODO: merge with similar code in FtpHandleUserRequest()
++ debugs(33, 5, "will need to re-login due to FTP server closure");
++ ftp.readGreeting = false;
++ FtpChangeState(this, ConnStateData::FTP_BEGIN, "server closure");
++ // XXX: Not enough. Gateway::ServerStateData::sendCommand() will not
++ // re-login because clientState() is not ConnStateData::FTP_CONNECTED.
++ }
++
+ if (sawZeroReply && clientConnection != NULL) {
+ debugs(33, 3, "Closing client connection on pinned zero reply.");
clientConnection->close();
- */
}
++
}
void
--ConnStateData::pinConnection(const Comm::ConnectionPointer &pinServer, HttpRequest *request, CachePeer *aPeer, bool auth)
++ConnStateData::pinConnection(const Comm::ConnectionPointer &pinServer, HttpRequest *request, CachePeer *aPeer, bool auth, bool monitor)
{
- char desc[FD_DESC_SZ];
+ if (!Comm::IsConnOpen(pinning.serverConnection) ||
+ pinning.serverConnection->fd != pinServer->fd)
+ pinNewConnection(pinServer, request, aPeer, auth);
- startMonitoringPinnedConnection();
- if (Comm::IsConnOpen(pinning.serverConnection)) {
- if (pinning.serverConnection->fd == pinServer->fd) {
- startPinnedConnectionMonitoring();
- return;
- }
- }
++ if (monitor)
++ startPinnedConnectionMonitoring();
+}
- unpinConnection(); // closes pinned connection, if any, and resets fields
+void
+ConnStateData::pinNewConnection(const Comm::ConnectionPointer &pinServer, HttpRequest *request, CachePeer *aPeer, bool auth)
+{
+ unpinConnection(true); // closes pinned connection, if any, and resets fields
pinning.serverConnection = pinServer;
Params ¶ms = GetCommParams<Params>(pinning.closeHandler);
params.conn = pinning.serverConnection;
comm_add_close_handler(pinning.serverConnection->fd, pinning.closeHandler);
-
- startPinnedConnectionMonitoring();
}
-/// Assign a read handler to an idle pinned connection so that we can detect connection closures.
++/// [re]start monitoring pinned connection for server closures so that we can
++/// propagate them to an _idle_ client pinned to the server
+ void
+ ConnStateData::startPinnedConnectionMonitoring()
+ {
+ if (pinning.readHandler != NULL)
+ return; // already monitoring
+
+ typedef CommCbMemFunT<ConnStateData, CommIoCbParams> Dialer;
+ pinning.readHandler = JobCallback(33, 3,
+ Dialer, this, ConnStateData::clientPinnedConnectionRead);
+ static char unusedBuf[8];
+ comm_read(pinning.serverConnection, unusedBuf, sizeof(unusedBuf), pinning.readHandler);
+ }
+
+ void
+ ConnStateData::stopPinnedConnectionMonitoring()
+ {
+ if (pinning.readHandler != NULL) {
+ comm_read_cancel(pinning.serverConnection->fd, pinning.readHandler);
+ pinning.readHandler = NULL;
+ }
+ }
+
+ /// Our read handler called by Comm when the server either closes an idle pinned connection or
+ /// perhaps unexpectedly sends something on that idle (from Squid p.o.v.) connection.
+ void
+ ConnStateData::clientPinnedConnectionRead(const CommIoCbParams &io)
+ {
+ pinning.readHandler = NULL; // Comm unregisters handlers before calling
+
+ if (io.flag == COMM_ERR_CLOSING)
+ return; // close handler will clean up
+
+ // We could use getConcurrentRequestCount(), but this may be faster.
+ const bool clientIsIdle = !getCurrentContext();
+
+ debugs(33, 3, "idle pinned " << pinning.serverConnection << " read " <<
+ io.size << (clientIsIdle ? " with idle client" : ""));
+
+ assert(pinning.serverConnection == io.conn);
+ pinning.serverConnection->close();
+
+ // If we are still sending data to the client, do not close now. When we are done sending,
+ // ClientSocketContext::keepaliveNextRequest() checks pinning.serverConnection and will close.
+ // However, if we are idle, then we must close to inform the idle client and minimize races.
+ if (clientIsIdle && clientConnection != NULL)
+ clientConnection->close();
+ }
+
const Comm::ConnectionPointer
ConnStateData::validatePinnedConnection(HttpRequest *request, const CachePeer *aPeer)
{
return pinning.serverConnection;
}
- stopMonitoringPinnedConnection();
+Comm::ConnectionPointer
+ConnStateData::borrowPinnedConnection(HttpRequest *request, const CachePeer *aPeer)
+{
+ debugs(33, 7, pinning.serverConnection);
+ if (validatePinnedConnection(request, aPeer) != NULL)
- /// [re]start monitoring pinned connection for server closures so that we can
- /// propagate them to an _idle_ client pinned to the server
- void
- ConnStateData::startMonitoringPinnedConnection()
- {
- if (!pinning.reading) {
- pinning.reading = true;
- Comm::SetSelect(pinning.serverConnection->fd, COMM_SELECT_READ,
- &ConnStateData::ReadPinnedConnection,
- new Pointer(this), 0);
- }
- }
-
- /// stop or suspend monitoring pinned connection for server closures
- void
- ConnStateData::stopMonitoringPinnedConnection()
- {
- if (pinning.reading) {
- Comm::SetSelect(pinning.serverConnection->fd, COMM_SELECT_READ, NULL, NULL, 0);
- pinning.reading = false;
- }
- }
-
- /// read callback for the idle pinned server connection
- void
- ConnStateData::ReadPinnedConnection(int fd, void *data)
- {
- Pointer *ptr = static_cast<Pointer*>(data);
- if (ConnStateData *client = dynamic_cast<ConnStateData*>(ptr->valid())) {
- // get back inside job call protection
- typedef NullaryMemFunT<ConnStateData> Dialer;
- AsyncCall::Pointer call = JobCallback(33, 5, Dialer, client,
- ConnStateData::readPinnedConnection);
- ScheduleCallHere(call);
- }
- delete ptr;
- }
-
- void
- ConnStateData::readPinnedConnection()
- {
- pinning.reading = false; // select loop clears our subscription before cb
- mustStop("suspected pinned server eof");
- }
-
++ stopPinnedConnectionMonitoring();
+
+ return pinning.serverConnection; // closed if validation failed
+}
+
void
-ConnStateData::unpinConnection()
+ConnStateData::unpinConnection(const bool andClose)
{
debugs(33, 3, HERE << pinning.serverConnection);
comm_remove_close_handler(pinning.serverConnection->fd, pinning.closeHandler);
pinning.closeHandler = NULL;
}
- /// also close the server side socket, we should not use it for any future requests...
- // TODO: do not close if called from our close handler?
- pinning.serverConnection->close();
+
- stopMonitoringPinnedConnection();
++ stopPinnedConnectionMonitoring();
+
+ // close the server side socket if requested
+ if (andClose)
+ pinning.serverConnection->close();
+ pinning.serverConnection = NULL;
}
safe_free(pinning.host);
/* 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_cast<const char *>(memchr(connState->in.buf, '\n',
- min(connState->in.notYetUsed, Config.maxRequestHeaderSize)));
- const size_t req_sz = eor + 1 - connState->in.buf;
+
+const char *
+ConnStateData::ftpBuildUri(const char *file)
+{
+ ftp.uri = "ftp://";
+ ftp.uri.append(ftp.host);
+ if (port->ftp_track_dirs && ftp.workingDir.size()) {
+ if (ftp.workingDir[0] != '/')
+ ftp.uri.append("/");
+ ftp.uri.append(ftp.workingDir);
+ }
+
+ if (ftp.uri[ftp.uri.size() - 1] != '/')
+ ftp.uri.append("/");
+
+ if (port->ftp_track_dirs && file) {
+ //remove any '/' from the beginning of path
+ while (*file == '/')
+ ++file;
+ ftp.uri.append(file);
+ }
+
+ return ftp.uri.termedBuf();
+}
+
+void
+ConnStateData::ftpSetWorkingDir(const char *dir)
+{
+ ftp.workingDir = dir;
+}
+
+static void
+FtpAcceptDataConnection(const CommAcceptCbParams ¶ms)
+{
+ ConnStateData *connState = static_cast<ConnStateData *>(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, "accepted " << params.conn);
+ fd_note(params.conn->fd, "passive client ftp data");
+ ++incoming_sockets_accepted;
+
+ if (!connState->clientConnection) {
+ debugs(33, 5, "late data connection?");
+ FtpCloseDataConnection(connState); // in case we are still listening
+ params.conn->close();
+ } else
+ if (params.conn->remote != connState->clientConnection->remote) {
+ debugs(33, 2, "rogue data conn? ctrl: " << connState->clientConnection->remote);
+ params.conn->close();
+ // Some FTP servers close control connection here, but it may make
+ // things worse from DoS p.o.v. and no better from data stealing p.o.v.
+ } else {
+ FtpCloseDataConnection(connState);
+ connState->ftp.dataConn = params.conn;
+ connState->ftp.uploadAvailSize = 0;
+ debugs(33, 7, "ready for data");
+ if (connState->ftp.onDataAcceptCall != NULL) {
+ AsyncCall::Pointer call = connState->ftp.onDataAcceptCall;
+ connState->ftp.onDataAcceptCall = NULL;
+ // If we got an upload request, start reading data from the client.
+ if (connState->ftp.state == ConnStateData::FTP_HANDLE_UPLOAD_REQUEST)
+ connState->readSomeFtpData();
+ else
+ Must(connState->ftp.state == ConnStateData::FTP_HANDLE_DATA_REQUEST);
+ MemBuf mb;
+ mb.init();
+ mb.Printf("150 Data connection opened.\r\n");
+ Comm::Write(connState->clientConnection, &mb, call);
+ }
+ }
+}
+
+static void
+FtpCloseDataConnection(ConnStateData *conn)
+{
+ if (conn->ftp.listener != NULL) {
+ conn->ftp.listener->cancel("no longer needed");
+ conn->ftp.listener = NULL;
+ }
+
+ 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 (conn->ftp.reader != NULL) {
+ // comm_read_cancel can deal with negative FDs
+ comm_read_cancel(conn->ftp.dataConn->fd, conn->ftp.reader);
+ conn->ftp.reader = 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;
+}
+
+/// Writes FTP [error] response before we fully parsed the FTP request and
+/// created the corresponding HTTP request wrapper for that FTP request.
+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;
+
+ // TODO: Create master transaction. Log it in FtpWroteEarlyReply.
+}
+
+static void
+FtpWriteReply(ClientSocketContext *context, MemBuf &mb)
+{
+ debugs(11, 2, "FTP Client " << context->clientConnection);
+ debugs(11, 2, "FTP Client REPLY:\n---------\n" << mb.buf <<
+ "\n----------");
+
+ 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);
+}
+
+static void
+FtpChangeState(ConnStateData *connState, const ConnStateData::FtpState newState, const char *reason)
+{
+ assert(connState);
+ if (connState->ftp.state == newState) {
+ debugs(33, 3, "client state unchanged at " << connState->ftp.state <<
+ " because " << reason);
+ connState->ftp.state = newState;
+ } else {
+ debugs(33, 3, "client state was " << connState->ftp.state <<
+ ", now " << newState << " because " << reason);
+ connState->ftp.state = newState;
+ }
+}
+
+/** 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)
+{
+ *http_ver = Http::ProtocolVersion(1, 1);
+
++ // TODO: Use tokenizer for parsing instead of raw pointer manipulation.
++ const char *inBuf = connState->in.buf.rawContent();
++
+ const char *const eor =
- if (eor == NULL && connState->in.notYetUsed >= Config.maxRequestHeaderSize) {
++ static_cast<const char *>(memchr(inBuf, '\n',
++ min(static_cast<size_t>(connState->in.buf.length()), Config.maxRequestHeaderSize)));
+
- connNoteUseOfBuffer(connState, req_sz);
++ if (eor == NULL && connState->in.buf.length() >= Config.maxRequestHeaderSize) {
+ FtpChangeState(connState, ConnStateData::FTP_ERROR, "huge req");
+ FtpWriteEarlyReply(connState, 421, "Too large request");
+ return NULL;
+ }
+
+ if (eor == NULL) {
+ debugs(33, 5, HERE << "Incomplete request, waiting for end of request");
+ return NULL;
+ }
+
- const char *boc = connState->in.buf;
++ const size_t req_sz = eor + 1 - inBuf;
+
+ // skip leading whitespaces
- const char *eoc = boc;
++ const char *boc = inBuf; // beginning of command
+ while (boc < eor && isspace(*boc)) ++boc;
+ if (boc >= eor) {
+ debugs(33, 5, HERE << "Empty request, ignoring");
++ connNoteUseOfBuffer(connState, req_sz);
+ return NULL;
+ }
+
- connState->in.buf[eoc - connState->in.buf] = '\0';
++ const char *eoc = boc; // end of command
+ while (eoc < eor && !isspace(*eoc)) ++eoc;
- const char *bop = eoc + 1;
++ connState->in.buf.setAt(eoc - inBuf, '\0');
+
- connState->in.buf[eop + 1 - connState->in.buf] = '\0';
++ const char *bop = eoc + 1; // beginning of parameter
+ while (bop < eor && isspace(*bop)) ++bop;
+ if (bop < eor) {
+ const char *eop = eor - 1;
+ while (isspace(*eop)) --eop;
+ assert(eop >= bop);
- ClientSocketContextNew(connState->clientConnection, http);
++ connState->in.buf.setAt(eop + 1 - inBuf, '\0');
+ } else
+ bop = NULL;
+
+ debugs(33, 7, HERE << "Parsed FTP command " << boc << " with " <<
+ (bop == NULL ? "no " : "") << "parameters" <<
+ (bop != NULL ? ": " : "") << bop);
+
++ // TODO: Use SBuf instead of String
+ const String cmd = boc;
+ String params = bop;
+
++ connNoteUseOfBuffer(connState, req_sz);
++
+ if (!connState->ftp.readGreeting) {
+ // the first command must be USER
+ if (!connState->pinning.pinned && cmd.caseCmp("USER") != 0) {
+ FtpWriteEarlyReply(connState, 530, "Must login first");
+ return NULL;
+ }
+ }
+
+ // We need to process USER request now because it sets ftp server Hostname.
+ if (cmd.caseCmp("USER") == 0 &&
+ !FtpHandleUserRequest(connState, cmd, params))
+ return NULL;
+
+ if (!FtpSupportedCommand(cmd)) {
+ FtpWriteEarlyReply(connState, 502, "Unknown or unsupported command");
+ return NULL;
+ }
+
+ *method_p = !cmd.caseCmp("APPE") || !cmd.caseCmp("STOR") ||
+ !cmd.caseCmp("STOU") ? Http::METHOD_PUT : Http::METHOD_GET;
+
+ char *uri;
+ const char *aPath = params.size() > 0 && Ftp::hasPathParameter(cmd)?
+ params.termedBuf() : NULL;
+ uri = xstrdup(connState->ftpBuildUri(aPath));
+ 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();
+ safe_free(uri);
+ return NULL;
+ }
+
+ request->http_ver = *http_ver;
+
+ // Our fake Request-URIs are not distinctive enough for caching to work
+ request->flags.cachable = false; // XXX: reset later by maybeCacheable()
+ request->flags.noCache = true;
+
+ request->header.putStr(HDR_FTP_COMMAND, cmd.termedBuf());
+ request->header.putStr(HDR_FTP_ARGUMENTS, params.termedBuf() != NULL ?
+ params.termedBuf() : "");
+ if (*method_p == Http::METHOD_PUT) {
+ request->header.putStr(HDR_EXPECT, "100-continue");
+ request->header.putStr(HDR_TRANSFER_ENCODING, "chunked");
+ }
+
+ ClientHttpRequest *const http = new ClientHttpRequest(connState);
+ http->request = request;
+ HTTPMSGLOCK(http->request);
+ http->req_sz = req_sz;
+ http->uri = uri;
+
+ ClientSocketContext *const result =
++ new ClientSocketContext(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->registerWithConn();
+ result->flags.parsed_ok = 1;
+ connState->flags.readMore = false;
+ return result;
+}
+
+static void
+FtpHandleReply(ClientSocketContext *context, HttpReply *reply, StoreIOBuffer data)
+{
+ if (context->http && context->http->al != NULL &&
+ !context->http->al->reply && reply) {
+ context->http->al->reply = reply;
+ HTTPMSGLOCK(context->http->al->reply);
+ }
+
+ static FtpReplyHandler *handlers[] = {
+ NULL, // FTP_BEGIN
+ NULL, // FTP_CONNECTED
+ FtpHandleFeatReply, // FTP_HANDLE_FEAT
+ FtpHandlePasvReply, // FTP_HANDLE_PASV
+ FtpHandlePortReply, // FTP_HANDLE_PORT
+ FtpHandleDataReply, // FTP_HANDLE_DATA_REQUEST
+ FtpHandleUploadReply, // FTP_HANDLE_UPLOAD_REQUEST
+ FtpHandleEprtReply,// FTP_HANDLE_EPRT
+ FtpHandleEpsvReply,// FTP_HANDLE_EPSV
+ NULL, // FTP_HANDLE_CWD
+ NULL, //FTP_HANDLE_PASS
+ NULL, // FTP_HANDLE_CDUP
+ FtpHandleErrorReply // FTP_ERROR
+ };
+ const ConnStateData::FtpState state = context->getConn()->ftp.state;
+ FtpReplyHandler *const handler = handlers[state];
+ if (handler)
+ (*handler)(context, reply, data);
+ else
+ FtpWriteForwardedReply(context, reply);
+}
+
+static void
+FtpHandleFeatReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ if (context->http->request->errType != ERR_NONE) {
+ FtpWriteCustomReply(context, 502, "Server does not support FEAT", reply);
+ return;
+ }
+
+ HttpReply *filteredReply = reply->clone();
+ HttpHeader &filteredHeader = filteredReply->header;
+
+ // Remove all unsupported commands from the response wrapper.
+ int deletedCount = 0;
+ HttpHeaderPos pos = HttpHeaderInitPos;
+ bool hasEPRT = false;
+ bool hasEPSV = false;
+ int prependSpaces = 1;
+ while (const HttpHeaderEntry *e = filteredHeader.getEntry(&pos)) {
+ if (e->id == HDR_FTP_PRE) {
+ // assume RFC 2389 FEAT response format, quoted by Squid:
+ // <"> SP NAME [SP PARAMS] <">
+ // but accommodate MS servers sending four SPs before NAME
+ if (e->value.size() < 4)
+ continue;
+ const char *raw = e->value.termedBuf();
+ if (raw[0] != '"' || raw[1] != ' ')
+ continue;
+ const char *beg = raw + 1 + strspn(raw + 1, " "); // after quote and spaces
+ // command name ends with (SP parameter) or quote
+ const char *end = beg + strcspn(beg, " \"");
+
+ if (end <= beg)
+ continue;
+
+ // compute the number of spaces before the command
+ prependSpaces = beg - raw - 1;
+
+ const String cmd = e->value.substr(beg-raw, end-raw);
+
+ if (!FtpSupportedCommand(cmd))
+ filteredHeader.delAt(pos, deletedCount);
+
+ if (cmd == "EPRT")
+ hasEPRT = true;
+ else if (cmd == "EPSV")
+ hasEPSV = true;
+ }
+ }
+
+ char buf[256];
+ int insertedCount = 0;
+ if (!hasEPRT) {
+ snprintf(buf, sizeof(buf), "\"%*s\"", prependSpaces + 4, "EPRT");
+ filteredHeader.putStr(HDR_FTP_PRE, buf);
+ ++insertedCount;
+ }
+ if (!hasEPSV) {
+ snprintf(buf, sizeof(buf), "\"%*s\"", prependSpaces + 4, "EPSV");
+ filteredHeader.putStr(HDR_FTP_PRE, buf);
+ ++insertedCount;
+ }
+
+ if (deletedCount || insertedCount) {
+ filteredHeader.refreshMask();
+ debugs(33, 5, "deleted " << deletedCount << " inserted " << insertedCount);
+ }
+
+ FtpWriteForwardedReply(context, filteredReply);
+}
+
+static void
+FtpHandlePasvReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ 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;
+ ConnStateData * const connState = context->getConn();
+ conn->flags = COMM_NONBLOCKING;
+ conn->local = connState->transparent() ?
+ connState->port->s : context->clientConnection->local;
+ conn->local.port(0);
+ 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<CommAcceptCbPtrFun> AcceptCall;
+ RefCount<AcceptCall> subCall = commCbCall(5, 5, "FtpAcceptDataConnection",
+ CommAcceptCbPtrFun(FtpAcceptDataConnection, connState));
+ Subscription::Pointer sub = new CallSubscription<AcceptCall>(subCall);
+ connState->ftp.listener = subCall.getRaw();
+ connState->ftp.dataListenConn = conn;
+ AsyncJob::Start(new Comm::TcpAcceptor(conn, note, sub));
+
+ char addr[MAX_IPSTRLEN];
+ // remote server in interception setups and local address otherwise
+ const Ip::Address &server = connState->transparent() ?
+ context->clientConnection->local : conn->local;
+ server.toStr(addr, MAX_IPSTRLEN, AF_INET);
+ addr[MAX_IPSTRLEN - 1] = '\0';
+ for (char *c = addr; *c != '\0'; ++c) {
+ if (*c == '.')
+ *c = ',';
+ }
+
+ // conn->fd is the client data connection (and its local port)
+ const unsigned short port = comm_local_port(conn->fd);
+ conn->local.port(port);
+
+ // In interception setups, we combine remote server address with a
+ // local port number and hope that traffic will be redirected to us.
+ MemBuf mb;
+ mb.init();
+
+ // Do not use "227 =a,b,c,d,p1,p2" format or omit parens: some nf_ct_ftp
+ // versions block responses that use those alternative syntax rules!
+ mb.Printf("227 Entering Passive Mode (%s,%i,%i).\r\n",
+ addr,
+ static_cast<int>(port / 256),
+ static_cast<int>(port % 256));
+
+ debugs(11, 3, Raw("writing", mb.buf, mb.size));
+ FtpWriteReply(context, mb);
+}
+
+static void
+FtpHandlePortReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ if (context->http->request->errType != ERR_NONE) {
+ FtpWriteCustomReply(context, 502, "Server does not support PASV (converted from PORT)", reply);
+ return;
+ }
+
+ FtpWriteCustomReply(context, 200, "PORT successfully converted to PASV.");
+
+ // and wait for RETR
+}
+
+static void
+FtpHandleErrorReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ ConnStateData *const connState = context->getConn();
+ if (!connState->pinning.pinned) // we failed to connect to server
+ connState->ftp.uri.clean();
+ // 421: we will close due to FTP_ERROR
+ FtpWriteErrorReply(context, reply, 421);
+}
+
+static void
+FtpHandleDataReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ ConnStateData *const conn = context->getConn();
+
+ if (reply != NULL && reply->sline.status() != Http::scOkay) {
+ FtpWriteForwardedReply(context, reply);
+ if (conn && Comm::IsConnOpen(conn->ftp.dataConn)) {
+ debugs(33, 3, "closing " << conn->ftp.dataConn << " on KO reply");
+ FtpCloseDataConnection(conn);
+ }
+ return;
+ }
+
+ if (!conn->ftp.dataConn) {
+ // We got STREAM_COMPLETE (or error) and closed the client data conn.
+ debugs(33, 3, "ignoring FTP srv data response after clt data closure");
+ return;
+ }
+
+ if (!FtpCheckDataConnPost(context)) {
+ FtpWriteCustomReply(context, 425, "Data connection is not established.");
+ FtpCloseDataConnection(conn);
+ return;
+ }
+
+ debugs(33, 7, HERE << data.length);
+
+ if (data.length <= 0) {
+ FtpWroteReplyData(conn->clientConnection, NULL, 0, COMM_OK, 0, context);
+ return;
+ }
+
+ MemBuf mb;
+ mb.init(data.length + 1, data.length + 1);
+ 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<ClientSocketContext*>(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;
+ }
+
+ assert(context->http);
+ context->http->out.size += size;
+
+ switch (context->socketState()) {
+ case STREAM_NONE:
+ debugs(33, 3, "Keep going");
+ 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
+FtpHandleUploadReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ FtpWriteForwardedReply(context, reply);
+ // note that the client data connection may already be closed by now
+}
+
+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
+FtpHandleEprtReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ if (context->http->request->errType != ERR_NONE) {
+ FtpWriteCustomReply(context, 502, "Server does not support PASV (converted from EPRT)", reply);
+ return;
+ }
+
+ FtpWriteCustomReply(context, 200, "EPRT successfully converted to PASV.");
+
+ // and wait for RETR
+}
+
+static void
+FtpHandleEpsvReply(ClientSocketContext *context, const HttpReply *reply, StoreIOBuffer data)
+{
+ if (context->http->request->errType != ERR_NONE) {
+ FtpWriteCustomReply(context, 502, "Cannot connect to server", reply);
+ return;
+ }
+
+ FtpCloseDataConnection(context->getConn());
+
+ Comm::ConnectionPointer conn = new Comm::Connection;
+ ConnStateData * const connState = context->getConn();
+ conn->flags = COMM_NONBLOCKING;
+ conn->local = connState->transparent() ?
+ connState->port->s : context->clientConnection->local;
+ conn->local.port(0);
+ 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, "comm_open_listener failed: " <<
+ conn->local << " error: " << errno);
+ FtpWriteCustomReply(context, 451, "Internal error");
+ return;
+ }
+
+ typedef CommCbFunPtrCallT<CommAcceptCbPtrFun> AcceptCall;
+ RefCount<AcceptCall> subCall = commCbCall(5, 5, "FtpAcceptDataConnection",
+ CommAcceptCbPtrFun(FtpAcceptDataConnection, connState));
+ Subscription::Pointer sub = new CallSubscription<AcceptCall>(subCall);
+ connState->ftp.listener = subCall.getRaw();
+ connState->ftp.dataListenConn = conn;
+ AsyncJob::Start(new Comm::TcpAcceptor(conn, note, sub));
+
+ // conn->fd is the client data connection (and its local port)
+ const unsigned int port = comm_local_port(conn->fd);
+ conn->local.port(port);
+
+ // In interception setups, we combine remote server address with a
+ // local port number and hope that traffic will be redirected to us.
+ MemBuf mb;
+ mb.init();
+ mb.Printf("229 Entering Extended Passive Mode (|||%u|)\r\n", port);
+
+ debugs(11, 3, Raw("writing", mb.buf, mb.size));
+ FtpWriteReply(context, mb);
+}
+
+/// writes FTP error response with given status and reply-derived error details
+static void
+FtpWriteErrorReply(ClientSocketContext *context, const HttpReply *reply, const int status)
+{
+ MemBuf mb;
+ mb.init();
+
+ assert(context->http);
+ const HttpRequest *request = context->http->request;
+ assert(request);
+ if (request->errType != ERR_NONE)
+ mb.Printf("%i-%s\r\n", status, errorPageName(request->errType));
+
+ if (request->errDetail > 0) {
+ // XXX: > 0 may not always mean that this is an errno
+ mb.Printf("%i-Error: (%d) %s\r\n", status,
+ request->errDetail,
+ strerror(request->errDetail));
+ }
+
+ // XXX: Remove hard coded names. Use an error page template instead.
+ const Adaptation::History::Pointer ah = request->adaptHistory();
+ if (ah != NULL) { // XXX: add adapt::<all_h but use lastMeta here
+ const String info = ah->allMeta.getByName("X-Response-Info");
+ const String desc = ah->allMeta.getByName("X-Response-Desc");
+ if (info.size())
+ mb.Printf("%i-Information: %s\r\n", status, info.termedBuf());
+ if (desc.size())
+ mb.Printf("%i-Description: %s\r\n", status, desc.termedBuf());
+ }
+
+ assert(reply != NULL);
+ const char *reason = reply->header.has(HDR_FTP_REASON) ?
+ reply->header.getStr(HDR_FTP_REASON):
+ reply->sline.reason();
+
+ mb.Printf("%i %s\r\n", status, reason); // error terminating line
+
+ // TODO: errorpage.cc should detect FTP client and use
+ // configurable FTP-friendly error templates which we should
+ // write to the client "as is" instead of hiding most of the info
+
+ FtpWriteReply(context, mb);
+}
+
+/// writes FTP response based on HTTP reply that is not an FTP-response wrapper
+static void
+FtpWriteForwardedForeign(ClientSocketContext *context, const HttpReply *reply)
+{
+ ConnStateData *const connState = context->getConn();
+ FtpChangeState(connState, ConnStateData::FTP_CONNECTED, "foreign reply");
+ // 451: We intend to keep the control connection open.
+ FtpWriteErrorReply(context, reply, 451);
+}
+
+static void
+FtpWriteForwardedReply(ClientSocketContext *context, const HttpReply *reply, AsyncCall::Pointer call)
+{
+ assert(reply != NULL);
+ const HttpHeader &header = reply->header;
+ ConnStateData *const connState = context->getConn();
+
+ // adaptation and forwarding errors lack HDR_FTP_STATUS
+ if (!header.has(HDR_FTP_STATUS)) {
+ FtpWriteForwardedForeign(context, reply);
+ return;
+ }
+
+ assert(header.has(HDR_FTP_REASON));
+
+ const int status = header.getInt(HDR_FTP_STATUS);
+ debugs(33, 7, HERE << "status: " << status);
+
+ // Status 125 or 150 implies upload or data request, but we still check
+ // the state in case the server is buggy.
+ if ((status == 125 || status == 150) &&
+ (connState->ftp.state == ConnStateData::FTP_HANDLE_UPLOAD_REQUEST ||
+ connState->ftp.state == ConnStateData::FTP_HANDLE_DATA_REQUEST)) {
+ if (FtpCheckDataConnPost(context)) {
+ // If the data connection is ready, start reading data (here)
+ // and forward the response to client (further below).
+ debugs(33, 7, "data connection established, start data transfer");
+ if (connState->ftp.state == ConnStateData::FTP_HANDLE_UPLOAD_REQUEST)
+ connState->readSomeFtpData();
+ } else {
+ // If we are waiting to accept the data connection, keep waiting.
+ if (Comm::IsConnOpen(connState->ftp.dataListenConn)) {
+ debugs(33, 7, "wait for the client to establish a data connection");
+ connState->ftp.onDataAcceptCall = call;
+ // TODO: Add connect timeout for passive connections listener?
+ // TODO: Remember server response so that we can forward it?
+ } else {
+ // Either the connection was establised and closed after the
+ // data was transferred OR we failed to establish an active
+ // data connection and already sent the error to the client.
+ // In either case, there is nothing more to do.
+ debugs(33, 7, "done with data OR active connection failed");
+ }
+ return;
+ }
+ }
+
+ MemBuf mb;
+ mb.init();
+ FtpPrintReply(mb, reply);
+
+ debugs(11, 2, "FTP Client " << context->clientConnection);
+ debugs(11, 2, "FTP Client REPLY:\n---------\n" << mb.buf <<
+ "\n----------");
+
+ Comm::Write(context->clientConnection, &mb, call);
+}
+
+static void
+FtpPrintReply(MemBuf &mb, const HttpReply *reply, const char *const prefix)
+{
+ const HttpHeader &header = reply->header;
+
+ HttpHeaderPos pos = HttpHeaderInitPos;
+ while (const HttpHeaderEntry *e = header.getEntry(&pos)) {
+ if (e->id == HDR_FTP_PRE) {
+ String raw;
+ if (httpHeaderParseQuotedString(e->value.rawBuf(), e->value.size(), &raw))
+ mb.Printf("%s\r\n", raw.termedBuf());
+ }
+ }
+
+ if (header.has(HDR_FTP_STATUS)) {
+ const char *reason = header.getStr(HDR_FTP_REASON);
+ mb.Printf("%i %s\r\n", header.getInt(HDR_FTP_STATUS),
+ (reason ? reason : 0));
+ }
+}
+
+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<ConnStateData*>(data);
+ ClientSocketContext::Pointer context = connState->getCurrentContext();
+ if (context != NULL && context->http) {
+ context->http->out.size += size;
+ context->http->out.headers_sz += size;
+ }
+
+ 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<ClientSocketContext*>(data);
+ ConnStateData *const connState = context->getConn();
+
+ assert(context->http);
+ context->http->out.size += size;
+ context->http->out.headers_sz += size;
+
+ if (connState->ftp.state == ConnStateData::FTP_ERROR) {
+ debugs(33, 5, "closing on FTP server error");
+ conn->close();
+ return;
+ }
+
+ const clientStream_status_t socketState = context->socketState();
+ debugs(33, 5, "FTP client stream state " << socketState);
+ switch (socketState) {
+ case STREAM_UNPLANNED_COMPLETE:
+ case STREAM_FAILED:
+ conn->close();
+ return;
+
+ case STREAM_NONE:
+ case STREAM_COMPLETE:
+ connState->flags.readMore = true;
+ FtpChangeState(connState, ConnStateData::FTP_CONNECTED, "FtpWroteReply");
+ if (connState->in.bodyParser)
+ connState->finishDechunkingRequest(false);
+ context->keepaliveNextRequest();
+ return;
+ }
+}
+
+bool
+FtpHandleRequest(ClientSocketContext *context, String &cmd, String ¶ms) {
+ if (HttpRequest *request = context->http->request) {
+ MemBuf *mb = new MemBuf;
+ Packer p;
+ mb->init();
+ packerToMemInit(&p, mb);
+ request->pack(&p);
+ packerClean(&p);
+
+ debugs(11, 2, "FTP Client " << context->clientConnection);
+ debugs(11, 2, "FTP Client REQUEST:\n---------\n" << mb->buf <<
+ "\n----------");
+ delete mb;
+ }
+
+ static std::pair<const char *, FtpRequestHandler *> handlers[] = {
+ std::make_pair("LIST", FtpHandleDataRequest),
+ std::make_pair("NLST", FtpHandleDataRequest),
+ std::make_pair("MLSD", FtpHandleDataRequest),
+ std::make_pair("FEAT", FtpHandleFeatRequest),
+ std::make_pair("PASV", FtpHandlePasvRequest),
+ std::make_pair("PORT", FtpHandlePortRequest),
+ std::make_pair("RETR", FtpHandleDataRequest),
+ std::make_pair("EPRT", FtpHandleEprtRequest),
+ std::make_pair("EPSV", FtpHandleEpsvRequest),
+ std::make_pair("CWD", FtpHandleCwdRequest),
+ std::make_pair("PASS", FtpHandlePassRequest),
+ std::make_pair("CDUP", FtpHandleCdupRequest),
+ };
+
+ FtpRequestHandler *handler = NULL;
+ if (context->http->request->method == Http::METHOD_PUT)
+ handler = FtpHandleUploadRequest;
+ else {
+ 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)(context, cmd, params) : true;
+}
+
+/// Called to parse USER command, which is required to create an HTTP request
+/// wrapper. Thus, errors are handled with FtpWriteEarlyReply() here.
+bool
+FtpHandleUserRequest(ConnStateData *connState, const String &cmd, String ¶ms)
+{
+ if (params.size() == 0) {
+ FtpWriteEarlyReply(connState, 501, "Missing username");
+ return false;
+ }
+
+ const String::size_type eou = params.rfind('@');
+ if (eou == String::npos || eou + 1 >= params.size()) {
+ FtpWriteEarlyReply(connState, 501, "Missing host");
+ return false;
+ }
+
+ const String login = params.substr(0, eou);
+ String host = params.substr(eou + 1, params.size());
+ // If we can parse it as raw IPv6 address, then surround with "[]".
+ // Otherwise (domain, IPv4, [bracketed] IPv6, garbage, etc), use as is.
+ if (host.pos(":")) {
+ char ipBuf[MAX_IPSTRLEN];
+ Ip::Address ipa;
+ ipa = host.termedBuf();
+ if (!ipa.isAnyAddr()) {
+ ipa.toHostStr(ipBuf, MAX_IPSTRLEN);
+ host = ipBuf;
+ }
+ }
+ connState->ftp.host = host;
+
+ String oldUri;
+ if (connState->ftp.readGreeting)
+ oldUri = connState->ftp.uri;
+
+ connState->ftpSetWorkingDir(NULL);
+ connState->ftpBuildUri();
+
+ if (!connState->ftp.readGreeting) {
+ debugs(11, 3, "set URI to " << connState->ftp.uri);
+ } else if (oldUri.caseCmp(connState->ftp.uri) == 0) {
+ debugs(11, 5, "keep URI as " << oldUri);
+ } else {
+ debugs(11, 3, "reset URI from " << oldUri << " to " << connState->ftp.uri);
+ FtpCloseDataConnection(connState);
+ connState->ftp.readGreeting = false;
+ connState->unpinConnection(true); // close control connection to the server
+ FtpChangeState(connState, ConnStateData::FTP_BEGIN, "URI reset");
+ }
+
+ params.cut(eou);
+
+ return true;
+}
+
+bool
+FtpHandleFeatRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_FEAT, "FtpHandleFeatRequest");
+
+ return true;
+}
+
+bool
+FtpHandlePasvRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ ConnStateData *const connState = context->getConn();
+ assert(connState);
+ if (connState->ftp.gotEpsvAll) {
+ FtpSetReply(context, 500, "Bad PASV command");
+ return false;
+ }
+
+ if (params.size() > 0) {
+ FtpSetReply(context, 501, "Unexpected parameter");
+ return false;
+ }
+
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_PASV, "FtpHandlePasvRequest");
+ // no need to fake PASV request via FtpSetDataCommand() in true PASV case
+ return true;
+}
+
+/// [Re]initializes dataConn for active data transfers. Does not connect.
+static
+bool FtpCreateDataConnection(ClientSocketContext *context, Ip::Address cltAddr)
+{
+ ConnStateData *const connState = context->getConn();
+ assert(connState);
+ assert(connState->clientConnection != NULL);
+ assert(!connState->clientConnection->remote.isAnyAddr());
+
+ if (cltAddr != connState->clientConnection->remote) {
+ debugs(33, 2, "rogue PORT " << cltAddr << " request? ctrl: " << connState->clientConnection->remote);
+ // Closing the control connection would not help with attacks because
+ // the client is evidently able to connect to us. Besides, closing
+ // makes retrials easier for the client and more damaging to us.
+ FtpSetReply(context, 501, "Prohibited parameter value");
+ return false;
+ }
+
+ FtpCloseDataConnection(context->getConn());
+
+ Comm::ConnectionPointer conn = new Comm::Connection();
+ conn->remote = cltAddr;
+
+ // Use local IP address of the control connection as the source address
+ // of the active data connection, or some clients will refuse to accept.
+ conn->flags |= COMM_DOBIND;
+ conn->local = connState->clientConnection->local;
+ // RFC 959 requires active FTP connections to originate from port 20
+ // but that would preclude us from supporting concurrent transfers! (XXX?)
+ conn->local.port(0);
+
+ debugs(11, 3, "will actively connect from " << conn->local << " to " <<
+ conn->remote);
+
+ context->getConn()->ftp.dataConn = conn;
+ context->getConn()->ftp.uploadAvailSize = 0;
+ return true;
+}
+
+bool
+FtpHandlePortRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ // TODO: Should PORT errors trigger FtpCloseDataConnection() cleanup?
+
+ const ConnStateData *connState = context->getConn();
+ if (connState->ftp.gotEpsvAll) {
+ FtpSetReply(context, 500, "Rejecting PORT after EPSV ALL");
+ return false;
+ }
+
+ if (!params.size()) {
+ FtpSetReply(context, 501, "Missing parameter");
+ return false;
+ }
+
+ Ip::Address cltAddr;
+ if (!Ftp::ParseIpPort(params.termedBuf(), NULL, cltAddr)) {
+ FtpSetReply(context, 501, "Invalid parameter");
+ return false;
+ }
+
+ if (!FtpCreateDataConnection(context, cltAddr))
+ return false;
+
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_PORT, "FtpHandlePortRequest");
+ FtpSetDataCommand(context);
+ return true; // forward our fake PASV request
+}
+
+bool
+FtpHandleDataRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ if (!FtpCheckDataConnPre(context))
+ return false;
+
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_DATA_REQUEST, "FtpHandleDataRequest");
+
+ return true;
+}
+
+bool
+FtpHandleUploadRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ if (!FtpCheckDataConnPre(context))
+ return false;
+
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_UPLOAD_REQUEST, "FtpHandleDataRequest");
+
+ return true;
+}
+
+bool
+FtpHandleEprtRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ debugs(11, 3, "Process an EPRT " << params);
+
+ const ConnStateData *connState = context->getConn();
+ if (connState->ftp.gotEpsvAll) {
+ FtpSetReply(context, 500, "Rejecting EPRT after EPSV ALL");
+ return false;
+ }
+
+ if (!params.size()) {
+ FtpSetReply(context, 501, "Missing parameter");
+ return false;
+ }
+
+ Ip::Address cltAddr;
+ if (!Ftp::ParseProtoIpPort(params.termedBuf(), cltAddr)) {
+ FtpSetReply(context, 501, "Invalid parameter");
+ return false;
+ }
+
+ if (!FtpCreateDataConnection(context, cltAddr))
+ return false;
+
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_EPRT, "FtpHandleEprtRequest");
+ FtpSetDataCommand(context);
+ return true; // forward our fake PASV request
+}
+
+bool
+FtpHandleEpsvRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ debugs(11, 3, "Process an EPSV command with params: " << params);
+ if (params.size() <= 0) {
+ // treat parameterless EPSV as "use the protocol of the ctrl conn"
+ } else if (params.caseCmp("ALL") == 0) {
+ ConnStateData *connState = context->getConn();
+ FtpSetReply(context, 200, "EPSV ALL ok");
+ connState->ftp.gotEpsvAll = true;
+ return false;
+ } else if (params.cmp("2") == 0) {
+ if (!Ip::EnableIpv6) {
+ FtpSetReply(context, 522, "Network protocol not supported, use (1)");
+ return false;
+ }
+ } else if (params.cmp("1") != 0) {
+ FtpSetReply(context, 501, "Unsupported EPSV parameter");
+ return false;
+ }
+
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_EPSV, "FtpHandleEpsvRequest");
+ FtpSetDataCommand(context);
+ return true; // forward our fake PASV request
+}
+
+bool
+FtpHandleCwdRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_CWD, "FtpHandleCwdRequest");
+ return true;
+}
+
+bool
+FtpHandlePassRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_PASS, "FtpHandlePassRequest");
+ return true;
+}
+
+bool
+FtpHandleCdupRequest(ClientSocketContext *context, String &cmd, String ¶ms)
+{
+ FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_CDUP, "FtpHandleCdupRequest");
+ return true;
+}
+
+// Convert client PORT, EPRT, PASV, or EPSV data command to Squid PASV command.
+// Squid server-side decides what data command to use on that side.
+void
+FtpSetDataCommand(ClientSocketContext *context)
+{
+ ClientHttpRequest *const http = context->http;
+ assert(http != NULL);
+ HttpRequest *const request = http->request;
+ assert(request != NULL);
+ HttpHeader &header = request->header;
+ header.delById(HDR_FTP_COMMAND);
+ header.putStr(HDR_FTP_COMMAND, "PASV");
+ header.delById(HDR_FTP_ARGUMENTS);
+ header.putStr(HDR_FTP_ARGUMENTS, "");
+ debugs(11, 5, "client data command converted to fake PASV");
+}
+
+/// check that client data connection is ready for future I/O or at least
+/// has a chance of becoming ready soon.
+bool
+FtpCheckDataConnPre(ClientSocketContext *context)
+{
+ ConnStateData *const connState = context->getConn();
+ if (Comm::IsConnOpen(connState->ftp.dataConn))
+ return true;
+
+ if (Comm::IsConnOpen(connState->ftp.dataListenConn)) {
+ // We are still waiting for a client to connect to us after PASV.
+ // Perhaps client's data conn handshake has not reached us yet.
+ // After we talk to the server, FtpCheckDataConnPost() will recheck.
+ debugs(33, 3, "expecting clt data conn " << connState->ftp.dataListenConn);
+ return true;
+ }
+
+ if (!connState->ftp.dataConn || connState->ftp.dataConn->remote.isAnyAddr()) {
+ debugs(33, 5, "missing " << connState->ftp.dataConn);
+ // TODO: use client address and default port instead.
+ FtpSetReply(context, 425, "Use PORT or PASV first");
+ return false;
+ }
+
+ // active transfer: open a connection from Squid to client
+ AsyncCall::Pointer connector = context->getConn()->ftp.connector =
+ commCbCall(17, 3, "FtpConnectDoneWrapper",
+ CommConnectCbPtrFun(FtpHandleConnectDone, context));
+
+ Comm::ConnOpener *cs = new Comm::ConnOpener(connState->ftp.dataConn,
+ connector,
+ Config.Timeout.connect);
+ AsyncJob::Start(cs);
+ return false; // ConnStateData::processFtpRequest waits FtpHandleConnectDone
+}
+
+/// Check that client data connection is ready for immediate I/O.
+static bool
+FtpCheckDataConnPost(ClientSocketContext *context)
+{
+ ConnStateData *connState = context->getConn();
+ assert(connState);
+ const Comm::ConnectionPointer &dataConn = connState->ftp.dataConn;
+ if (!Comm::IsConnOpen(dataConn)) {
+ debugs(33, 3, "missing client data conn: " << dataConn);
+ return false;
+ }
+ return true;
+}
+
+void
+FtpHandleConnectDone(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno, void *data)
+{
+ ClientSocketContext *context = static_cast<ClientSocketContext*>(data);
+ context->getConn()->ftp.connector = NULL;
+
+ if (status != COMM_OK) {
+ conn->close();
+ FtpSetReply(context, 425, "Cannot open data connection.");
+ assert(context->http && context->http->storeEntry() != NULL);
+ } else {
+ assert(context->getConn()->ftp.dataConn == conn);
+ assert(Comm::IsConnOpen(conn));
+ fd_note(conn->fd, "active client ftp data");
+ }
+ context->getConn()->resumeFtpRequest(context);
+}
+
+void
+FtpSetReply(ClientSocketContext *context, const int code, const char *msg)
+{
+ ClientHttpRequest *const http = context->http;
+ assert(http != NULL);
+ assert(http->storeEntry() == NULL);
+
+ HttpReply *const reply = new HttpReply;
+ reply->sline.set(Http::ProtocolVersion(1, 1), Http::scNoContent);
+ HttpHeader &header = reply->header;
+ header.putTime(HDR_DATE, squid_curtime);
+ {
+ HttpHdrCc cc;
+ cc.Private();
+ header.putCc(&cc);
+ }
+ header.putInt64(HDR_CONTENT_LENGTH, 0);
+ header.putInt(HDR_FTP_STATUS, code);
+ header.putStr(HDR_FTP_REASON, msg);
+ reply->hdrCacheInit();
+
+ setLogUri(http, urlCanonicalClean(http->request));
+
+ clientStreamNode *const node = context->getClientReplyContext();
+ clientReplyContext *const repContext =
+ dynamic_cast<clientReplyContext *>(node->data.getRaw());
+ assert(repContext != NULL);
+
+ RequestFlags flags;
+ flags.cachable = false; // force releaseRequest() in storeCreateEntry()
+ flags.noCache = true;
+ repContext->createStoreEntry(http->request->method, flags);
+ http->storeEntry()->replaceHttpReply(reply);
+}
+
+/// Whether Squid FTP gateway supports a given feature (e.g., a command).
+static bool
+FtpSupportedCommand(const String &name)
+{
+ static std::set<std::string> BlackList;
+ if (BlackList.empty()) {
+ /* Add FTP commands that Squid cannot gateway correctly */
+
+ // we probably do not support AUTH TLS.* and AUTH SSL,
+ // but let's disclaim all AUTH support to KISS, for now
+ BlackList.insert("AUTH");
+ }
+
+ // we claim support for all commands that we do not know about
+ return BlackList.find(name.termedBuf()) == BlackList.end();
+}