]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Merged from trunk (r13356).
authorAlex Rousskov <rousskov@measurement-factory.com>
Fri, 23 May 2014 06:11:56 +0000 (00:11 -0600)
committerAlex Rousskov <rousskov@measurement-factory.com>
Fri, 23 May 2014 06:11:56 +0000 (00:11 -0600)
Needs more work to handle FTP adaptation failures better.

22 files changed:
1  2 
src/FtpServer.cc
src/FwdState.cc
src/HttpHdrCc.h
src/HttpHeader.cc
src/HttpHeader.h
src/HttpHeaderTools.cc
src/HttpReply.h
src/Makefile.am
src/Server.cc
src/Server.h
src/SquidConfig.h
src/adaptation/ecap/ServiceRep.cc
src/anyp/PortCfg.cc
src/anyp/PortCfg.h
src/cache_cf.cc
src/cf.data.pre
src/client_side.cc
src/client_side.h
src/ftp.cc
src/main.cc
src/tests/stub_client_side.cc
src/tools.cc

index c0d735954a510a81c088b1507e0bb58c91960ead,0000000000000000000000000000000000000000..cc9ffaa4551d6c2710e7df78ab0d9fa451e8dcaf
mode 100644,000000..100644
--- /dev/null
@@@ -1,1214 -1,0 +1,1219 @@@
-     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();
 +}
diff --cc src/FwdState.cc
index 9d66a7a88738317fb0502604acacc779bee3bdbb,dbc6abeee5db184e4444d1e8473e57831551b6a2..cb0befaaa0dfd3b565a8dce6ab2f66122520cb73
@@@ -1043,13 -1043,6 +1044,16 @@@ FwdState::connectDone(const Comm::Conne
      }
  #endif
  
-     if (clientConnState->isFtp) {
 +    const CbcPointer<ConnStateData> &clientConnState =
 +        request->clientConnectionManager;
-                                        serverConnection()->getPeer(), false);
++    if (clientConnState.valid() && clientConnState->isFtp) {
++        // this is not an idle connection, so we do not want I/O monitoring
++        const bool monitor = false;
 +        clientConnState->pinConnection(serverConnection(), request,
++                                       serverConnection()->getPeer(), false,
++                                       monitor);
 +    }
 +
      dispatch();
  }
  
@@@ -1127,10 -1120,11 +1131,12 @@@ FwdState::connectStart(
          else
              serverConn = NULL;
          if (Comm::IsConnOpen(serverConn)) {
+             pinned_connection->stopPinnedConnectionMonitoring();
              flags.connected_okay = true;
              ++n_tries;
 +            request->hier.note(serverConn, request->GetHost());
              request->flags.pinned = true;
+             request->hier.note(serverConn, pinned_connection->pinning.host);
              if (pinned_connection->pinnedAuth())
                  request->flags.auth = true;
              comm_add_close_handler(serverConn->fd, fwdServerClosedWrapper, this);
diff --cc src/HttpHdrCc.h
index 1781c83d19444bdd59ad5fdbf1410b5192adf3f9,7a3904dd159c9c958f239f627383d135b0d90265..a8d40bb1f58a11eaa907712c127fafffdc3341c1
@@@ -75,10 -75,10 +75,10 @@@ 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())
+         if (private_.size() > 0)
              private_.append(",");
          private_.append(v);
      }
Simple merge
Simple merge
Simple merge
diff --cc src/HttpReply.h
Simple merge
diff --cc src/Makefile.am
index 52abf5faa3d8c8b613f925cd87e1cd6a22e14e11,488b6a9ccc4392f3e3e54f7c014e2f04c4e8b613..ae6743edb2c08be7c6d87eb2ba017a3aaae27547
@@@ -353,10 -366,6 +366,10 @@@ squid_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
-       FtpServer.h \
-       FtpServer.cc \
 +      FtpGatewayServer.h \
 +      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        Generic.h \
@@@ -1456,6 -1452,6 +1456,10 @@@ tests_testCacheManager_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
++      FtpGatewayServer.h \
++      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        gopher.h \
@@@ -1871,6 -1868,6 +1876,10 @@@ tests_testEvent_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
++      FtpGatewayServer.h \
++      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        gopher.h \
@@@ -2121,6 -2116,6 +2128,10 @@@ tests_testEventLoop_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
++      FtpGatewayServer.h \
++      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        gopher.h \
@@@ -2368,6 -2360,6 +2376,10 @@@ tests_test_http_range_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
++      FtpGatewayServer.h \
++      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        gopher.h \
@@@ -2666,6 -2666,6 +2686,10 @@@ tests_testHttpRequest_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
++      FtpGatewayServer.h \
++      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        gopher.h \
@@@ -3636,6 -3482,6 +3506,10 @@@ tests_testURL_SOURCES = 
        fqdncache.cc \
        ftp.h \
        ftp.cc \
++      FtpGatewayServer.h \
++      FtpGatewayServer.cc \
++      FtpServer.h \
++      FtpServer.cc \
        FwdState.cc \
        FwdState.h \
        gopher.h \
diff --cc src/Server.cc
Simple merge
diff --cc src/Server.h
Simple merge
index 4d3cb9c87826f888c1538dc4f3628728f79e52a5,97d09a31002ec3c081b6d5fd44b91ef2403f8a24..d7cb847b1b0f8457fb2a9193c634f61fb66fa9ff
@@@ -140,10 -138,9 +139,10 @@@ public
  
      struct {
          AnyP::PortCfg *http;
- #if USE_SSL
+ #if USE_OPENSSL
          AnyP::PortCfg *https;
  #endif
 +        AnyP::PortCfg *ftp;
      } Sockaddr;
  #if SQUID_SNMP
  
index 260850591bd68334f363aff07e66fb8ea1eb296e,2c50e5b7df90e835e3839a60a184bff7542ce46b..c567dc13032bb22a9b67a9f9dc0333a8a98f3c24
@@@ -76,6 -95,55 +95,55 @@@ Adaptation::Ecap::ConfigRep::visitEachO
          visitor.visit(Name(i->first), Area::FromTempString(i->second));
  }
  
 -    static const struct timeval maxTimeout {
+ /* Adaptation::Ecap::Engine */
+ int
+ Adaptation::Ecap::Engine::checkEvents(int)
+ {
+     // Start with the default I/O loop timeout, convert from milliseconds.
++    static const struct timeval maxTimeout = {
+         EVENT_LOOP_TIMEOUT/1000, // seconds
+         (EVENT_LOOP_TIMEOUT % 1000)*1000
+     }; // microseconds
+     struct timeval timeout = maxTimeout;
+     kickAsyncServices(timeout);
+     if (timeout.tv_sec == maxTimeout.tv_sec && timeout.tv_usec == maxTimeout.tv_usec)
+         return EVENT_IDLE;
+     debugs(93, 7, "timeout: " << timeout.tv_sec << "s+" << timeout.tv_usec << "us");
+     // convert back to milliseconds, avoiding int overflows
+     if (timeout.tv_sec >= std::numeric_limits<int>::max()/1000 - 1000)
+         return std::numeric_limits<int>::max();
+     else
+         return timeout.tv_sec*1000 + timeout.tv_usec/1000;
+ }
+ /// resumes async transactions (if any) and returns true if they set a timeout
+ void
+ Adaptation::Ecap::Engine::kickAsyncServices(timeval &timeout)
+ {
+     if (AsyncServices.empty())
+         return;
+     debugs(93, 3, "async services: " << AsyncServices.size());
+     // Activate waiting async transactions, if any.
+     typedef AdapterServices::iterator ASI;
+     for (ASI s = AsyncServices.begin(); s != AsyncServices.end(); ++s) {
+         assert(s->second);
+         s->second->resume(); // may call Ecap::Xaction::resume()
+     }
+     // Give services a chance to decrease the default timeout.
+     for (ASI s = AsyncServices.begin(); s != AsyncServices.end(); ++s) {
+         s->second->suspend(timeout);
+     }
+ }
+ /* Adaptation::Ecap::ServiceRep */
  Adaptation::Ecap::ServiceRep::ServiceRep(const ServiceConfigPointer &cfg):
          /*AsyncJob("Adaptation::Ecap::ServiceRep"),*/ Adaptation::Service(cfg),
          isDetached(false)
index 61981f67d734455259e6637c776b58d5526a6267,ca810cc7ece925243315cd753468de222e2250bd..6392d03d4e3836ee00d6548fd773fa1291d5c3f7
@@@ -13,16 -14,52 +14,53 @@@ CBDATA_NAMESPACED_CLASS_INIT(AnyP, Port
  int NHttpSockets = 0;
  int HttpSockets[MAXTCPLISTENPORTS];
  
- AnyP::PortCfg::PortCfg(const char *aProtocol) :
+ AnyP::PortCfg::PortCfg() :
          next(NULL),
-         protocol(xstrdup(aProtocol)),
+         s(),
+         transport(AnyP::PROTO_HTTP,1,1), // "Squid is an HTTP proxy", etc.
          name(NULL),
          defaultsite(NULL),
- #if USE_SSL
+         flags(),
+         allow_direct(false),
+         vhost(false),
+         actAsOrigin(false),
+         ignore_cc(false),
+         connection_auth_disabled(false),
+         vport(0),
+         disable_pmtu_discovery(0),
 -        listenConn()
++        listenConn(),
+ #if USE_OPENSSL
 -        ,cert(NULL),
++        cert(NULL),
+         key(NULL),
+         version(0),
+         cipher(NULL),
+         options(NULL),
+         clientca(NULL),
+         cafile(NULL),
+         capath(NULL),
+         crlfile(NULL),
+         dhfile(NULL),
+         sslflags(NULL),
+         sslContextSessionId(NULL),
+         generateHostCertificates(false),
          dynamicCertMemCacheSize(std::numeric_limits<size_t>::max()),
 -        sslOptions(0)
+         staticSslContext(),
+         signingCert(),
+         signPkey(),
+         certsToChain(),
+         untrustedSigningCert(),
+         untrustedSignPkey(),
+         clientVerifyCrls(),
+         clientCA(),
+         dhParams(),
+         contextMethod(),
+         sslContextFlags(0),
++        sslOptions(0),
  #endif
- {}
 +        ftp_track_dirs(false)
+ {
+     memset(&tcp_keepalive, 0, sizeof(tcp_keepalive));
+ }
  
  AnyP::PortCfg::~PortCfg()
  {
@@@ -146,3 -181,18 +183,21 @@@ AnyP::PortCfg::configureSslServerContex
  }
  #endif
  
 -    if (strcasecmp("http", aProtocol) != 0 || strcmp("HTTP/1.1", aProtocol) != 0)
+ void
+ AnyP::PortCfg::setTransport(const char *aProtocol)
+ {
+     // HTTP/1.0 not supported because we are version 1.1 which contains a superset of 1.0
+     // and RFC 2616 requires us to upgrade 1.0 to 1.1
 -    else if (strcasecmp("https", aProtocol) != 0 || strcmp("HTTPS/1.1", aProtocol) != 0)
++    if (strcasecmp("http", aProtocol) == 0 || strcmp("HTTP/1.1", aProtocol) == 0)
+         transport = AnyP::ProtocolVersion(AnyP::PROTO_HTTP, 1,1);
++    else if (strcasecmp("https", aProtocol) == 0 || strcmp("HTTPS/1.1", aProtocol) == 0)
+         transport = AnyP::ProtocolVersion(AnyP::PROTO_HTTPS, 1,1);
++    else if (strcasecmp("ftp", aProtocol) == 0)
++        transport = AnyP::ProtocolVersion(AnyP::PROTO_FTP, 1,0);
++
+     else
+         fatalf("http(s)_port protocol=%s is not supported\n", aProtocol);
+ }
Simple merge
diff --cc src/cache_cf.cc
Simple merge
diff --cc src/cf.data.pre
Simple merge
index 77b58f30a84fc367aee1546f3a1c52fc20842bef,5fa28947642d6e1fab493080093c7c0d04da5365..2f45c16af4d072c7f58d037cd038a7d18407402f
  #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
@@@ -202,13 -191,9 +196,10 @@@ CBDATA_CLASS_INIT(ClientSocketContext)
  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 *);
@@@ -236,55 -221,6 +227,56 @@@ static void clientUpdateSocketStats(Log
  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 &params);
 +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 &params);
 +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
  {
@@@ -323,28 -253,9 +315,28 @@@ ConnStateData::readSomeData(
  
      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)
  {
@@@ -716,21 -615,13 +702,13 @@@ ClientHttpRequest::logRequest(
  
      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;
  
@@@ -3136,22 -2937,14 +3089,18 @@@ ConnStateData::clientParseRequests(
          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) {
@@@ -3544,10 -3260,9 +3486,10 @@@ clientLifetimeTimeout(const CommTimeout
          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
@@@ -4800,35 -4427,26 +4746,40 @@@ ConnStateData::clientPinnedConnectionCl
      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 &params = 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 &params)
 +{
 +    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 &params) {
 +    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 &params)
 +{
 +    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 &params)
 +{
 +    FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_FEAT, "FtpHandleFeatRequest");
 +
 +    return true;
 +}
 +
 +bool
 +FtpHandlePasvRequest(ClientSocketContext *context, String &cmd, String &params)
 +{
 +    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 &params)
 +{
 +    // 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 &params)
 +{
 +    if (!FtpCheckDataConnPre(context))
 +        return false;
 +
 +    FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_DATA_REQUEST, "FtpHandleDataRequest");
 +
 +    return true;
 +}
 +
 +bool
 +FtpHandleUploadRequest(ClientSocketContext *context, String &cmd, String &params)
 +{
 +    if (!FtpCheckDataConnPre(context))
 +        return false;
 +
 +    FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_UPLOAD_REQUEST, "FtpHandleDataRequest");
 +
 +    return true;
 +}
 +
 +bool
 +FtpHandleEprtRequest(ClientSocketContext *context, String &cmd, String &params)
 +{
 +    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 &params)
 +{
 +    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 &params)
 +{
 +    FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_CWD, "FtpHandleCwdRequest");
 +    return true;
 +}
 +
 +bool
 +FtpHandlePassRequest(ClientSocketContext *context, String &cmd, String &params)
 +{
 +    FtpChangeState(context->getConn(), ConnStateData::FTP_HANDLE_PASS, "FtpHandlePassRequest");
 +    return true;
 +}
 +
 +bool
 +FtpHandleCdupRequest(ClientSocketContext *context, String &cmd, String &params)
 +{
 +    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();
 +}
index 6ad6f693341dce36818ce1e0b5b2248b50c7c65e,7ec69f182b567b62d766c7ba7cdb1176bd8a291e..edf6103d0fb0809ed64b06574689f93d7e85f93d
@@@ -190,8 -190,6 +191,7 @@@ public
      ~ConnStateData();
  
      void readSomeData();
-     int getAvailableBufferLength() const;
 +    void readSomeFtpData();
      bool areAllContextsForThisConnection() const;
      void freeAllContexts();
      void notifyAllContexts(const int xerrno); ///< tell everybody about the err
          int port;               /* port of pinned connection */
          bool pinned;             /* this connection was pinned */
          bool auth;               /* pinned for www authentication */
 +        bool reading;   ///< we are monitoring for server connection closure
          bool zeroReply; ///< server closed w/o response (ERR_ZERO_SIZE_OBJECT)
          CachePeer *peer;             /* CachePeer the connection goes via */
+         AsyncCall::Pointer readHandler; ///< detects serverConnection closure
          AsyncCall::Pointer closeHandler; /*The close handler for pinned server side connection*/
      } pinning;
  
      virtual void noteMoreBodySpaceAvailable(BodyPipe::Pointer);
      virtual void noteBodyConsumerAborted(BodyPipe::Pointer);
  
-     bool handleReadData(char *buf, size_t size);
+     bool handleReadData(SBuf *buf);
      bool handleRequestBodyData();
  
 -    /**
 -     * Correlate the current ConnStateData object with the pinning_fd socket descriptor.
 -     */
 -    void pinConnection(const Comm::ConnectionPointer &pinServerConn, HttpRequest *request, CachePeer *peer, bool auth);
 -    /**
 -     * Decorrelate the ConnStateData object from its pinned CachePeer
 -     */
 -    void unpinConnection();
 +    /// forward future client requests using the given server connection
-     /// monitor pinned server connection for server-side closures
-     void pinConnection(const Comm::ConnectionPointer &pinServerConn, HttpRequest *request, CachePeer *peer, bool auth);
++    /// optionally, monitor pinned server connection for server-side closures
++    void pinConnection(const Comm::ConnectionPointer &pinServerConn, HttpRequest *request, CachePeer *peer, bool auth, bool monitor = true);
 +    /// undo pinConnection() and, optionally, close the pinned connection
 +    void unpinConnection(const bool andClose);
 +    /// returns validated pinnned server connection (and stops its monitoring)
 +    Comm::ConnectionPointer borrowPinnedConnection(HttpRequest *request, const CachePeer *aPeer);
      /**
       * Checks if there is pinning info if it is valid. It can close the server side connection
       * if pinned info is not valid.
      /// the client-side-detected error response instead of getting stuck.
      void quitAfterError(HttpRequest *request); // meant to be private
  
- #if USE_SSL
+     /// The caller assumes responsibility for connection closure detection.
+     void stopPinnedConnectionMonitoring();
 +    const bool isFtp;
 +    enum FtpState {
 +        FTP_BEGIN,
 +        FTP_CONNECTED,
 +        FTP_HANDLE_FEAT,
 +        FTP_HANDLE_PASV,
 +        FTP_HANDLE_PORT,
 +        FTP_HANDLE_DATA_REQUEST,
 +        FTP_HANDLE_UPLOAD_REQUEST,
 +        FTP_HANDLE_EPRT,
 +        FTP_HANDLE_EPSV,
 +        FTP_HANDLE_CWD,
 +        FTP_HANDLE_PASS,
 +        FTP_HANDLE_CDUP,
 +        FTP_ERROR
 +    };
 +    struct {
 +        String uri;
 +        String host;
 +        String workingDir;
 +        FtpState state;
 +        bool readGreeting;
 +        bool gotEpsvAll; ///< restrict data conn setup commands to just EPSV
 +        AsyncCall::Pointer onDataAcceptCall; ///< who to call upon data connection acceptance
 +        Comm::ConnectionPointer dataListenConn;
 +        Comm::ConnectionPointer dataConn;
 +        Ip::Address serverDataAddr;
 +        char uploadBuf[CLIENT_REQ_BUF_SZ];
 +        size_t uploadAvailSize;
 +        AsyncCall::Pointer listener; ///< set when we are passively listening
 +        AsyncCall::Pointer connector; ///< set when we are actively connecting
 +        AsyncCall::Pointer reader; ///< set when we are reading FTP data
 +    } ftp;
 +    const char *ftpBuildUri(const char *file = NULL);
 +    void ftpSetWorkingDir(const char *dir);
 +
+ #if USE_OPENSSL
      /// called by FwdState when it is done bumping the server
      void httpsPeeked(Comm::ConnectionPointer serverConnection);
  
@@@ -426,17 -388,8 +430,13 @@@ private
      int connReadWasError(comm_err_t flag, int size, int xerrno);
      int connFinishedWithConn(int size);
      void clientAfterReadingRequests();
 +    void processFtpRequest(ClientSocketContext *const context);
 +    void handleFtpRequestData();
 +
      bool concurrentRequestQueueFilled() const;
  
-     void startMonitoringPinnedConnection();
-     void stopMonitoringPinnedConnection();
-     static void ReadPinnedConnection(int fd, void *data);
-     void readPinnedConnection();
 +    void pinNewConnection(const Comm::ConnectionPointer &pinServer, HttpRequest *request, CachePeer *aPeer, bool auth);
 +
  #if USE_AUTH
      /// some user details that can be used to perform authentication on this connection
      Auth::UserRequest::Pointer auth_;
diff --cc src/ftp.cc
index 8941c98037fbd53c758a8f5cdd4da1492e7a3952,10f96a1ad95c1ecf948590f8ad7f9e1a9a931d44..7c2090677bbc573f17de45deea4582964ca20fad
   */
  
  #include "squid.h"
+ #include "acl/FilledChecklist.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"
@@@ -1777,7 -2512,97 +1777,11 @@@ ftpSendPassive(FtpStateData * ftpState
          return;
      }
  
-     ftpState->sendPassive();
 -    /// Closes any old FTP-Data connection which may exist. */
 -    ftpState->data.close();
 -
 -    /** \par
 -      * Checks for previous EPSV/PASV failures on this server/session.
 -      * Diverts to EPRT immediately if they are not working. */
 -    if (!ftpState->flags.pasv_supported) {
 -        ftpSendEPRT(ftpState);
 -        return;
 -    }
 -
 -    /** \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 (ftpState->state) {
 -    case SENT_EPSV_ALL: /* EPSV ALL resulted in a bad response. Try ther EPSV methods. */
 -        ftpState->flags.epsv_all_sent = true;
 -        if (ftpState->ctrl.conn->local.isIPv6()) {
 -            debugs(9, 5, HERE << "FTP Channel is IPv6 (" << ftpState->ctrl.conn->remote << ") attempting EPSV 2 after EPSV ALL has failed.");
 -            snprintf(cbuf, CTRL_BUFLEN, "EPSV 2\r\n");
 -            ftpState->state = SENT_EPSV_2;
 -            break;
 -        }
 -        // else fall through to skip EPSV 2
 -
 -    case SENT_EPSV_2: /* EPSV IPv6 failed. Try EPSV IPv4 */
 -        if (ftpState->ctrl.conn->local.isIPv4()) {
 -            debugs(9, 5, HERE << "FTP Channel is IPv4 (" << ftpState->ctrl.conn->remote << ") attempting EPSV 1 after EPSV ALL has failed.");
 -            snprintf(cbuf, CTRL_BUFLEN, "EPSV 1\r\n");
 -            ftpState->state = SENT_EPSV_1;
 -            break;
 -        } else if (ftpState->flags.epsv_all_sent) {
 -            debugs(9, DBG_IMPORTANT, "FTP does not allow PASV method after 'EPSV ALL' has been sent.");
 -            ftpFail(ftpState);
 -            return;
 -        }
 -        // else fall through to skip EPSV 1
 -
 -    case SENT_EPSV_1: /* EPSV options exhausted. Try PASV now. */
 -        debugs(9, 5, HERE << "FTP Channel (" << ftpState->ctrl.conn->remote << ") rejects EPSV connection attempts. Trying PASV instead.");
 -        snprintf(cbuf, CTRL_BUFLEN, "PASV\r\n");
 -        ftpState->state = SENT_PASV;
 -        break;
 -
 -    default: {
 -        bool doEpsv = true;
 -        if (Config.accessList.ftp_epsv) {
 -            ACLFilledChecklist checklist(Config.accessList.ftp_epsv, ftpState->fwd->request, NULL);
 -            doEpsv = (checklist.fastCheck() == ACCESS_ALLOWED);
 -        }
 -        if (!doEpsv) {
 -            debugs(9, 5, HERE << "EPSV support manually disabled. Sending PASV for FTP Channel (" << ftpState->ctrl.conn->remote <<")");
 -            snprintf(cbuf, CTRL_BUFLEN, "PASV\r\n");
 -            ftpState->state = SENT_PASV;
 -        } else if (Config.Ftp.epsv_all) {
 -            debugs(9, 5, HERE << "EPSV ALL manually enabled. Attempting with FTP Channel (" << ftpState->ctrl.conn->remote <<")");
 -            snprintf(cbuf, CTRL_BUFLEN, "EPSV ALL\r\n");
 -            ftpState->state = SENT_EPSV_ALL;
 -            /* block other non-EPSV connections being attempted */
++    if (ftpState->sendPassive()) {
++        // SENT_EPSV_ALL blocks other non-EPSV connections being attempted
++        if (ftpState->state == Ftp::ServerStateData::SENT_EPSV_ALL)
+             ftpState->flags.epsv_all_sent = true;
 -        } else {
 -            if (ftpState->ctrl.conn->local.isIPv6()) {
 -                debugs(9, 5, HERE << "FTP Channel (" << ftpState->ctrl.conn->remote << "). Sending default EPSV 2");
 -                snprintf(cbuf, CTRL_BUFLEN, "EPSV 2\r\n");
 -                ftpState->state = SENT_EPSV_2;
 -            }
 -            if (ftpState->ctrl.conn->local.isIPv4()) {
 -                debugs(9, 5, HERE << "Channel (" << ftpState->ctrl.conn->remote <<"). Sending default EPSV 1");
 -                snprintf(cbuf, CTRL_BUFLEN, "EPSV 1\r\n");
 -                ftpState->state = SENT_EPSV_1;
 -            }
 -        }
 -    }
 -        break;
+     }
 -
 -    ftpState->writeCommand(cbuf);
 -
 -    /*
 -     * 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).
 -     */
 -    typedef CommCbMemFunT<FtpStateData, CommTimeoutCbParams> TimeoutDialer;
 -    AsyncCall::Pointer timeoutCall =  JobCallback(9, 5,
 -                                      TimeoutDialer, ftpState, FtpStateData::ftpTimeout);
 -    commSetConnTimeout(ftpState->ctrl.conn, Config.Timeout.connect, timeoutCall);
  }
  
  void
diff --cc src/main.cc
Simple merge
index 7637ebf9e04cbf63cb4a684498edfdd53ed46457,d1a4fc94e9a22f2ea1c5c0add44942353c00eac8..ce479480c92aa940cd9f9fc94b0ed05a4d0b6813
@@@ -54,10 -51,10 +51,10 @@@ void ConnStateData::stopSending(const c
  void ConnStateData::expectNoForwarding() STUB
  void ConnStateData::noteMoreBodySpaceAvailable(BodyPipe::Pointer) STUB
  void ConnStateData::noteBodyConsumerAborted(BodyPipe::Pointer) STUB
- bool ConnStateData::handleReadData(char *buf, size_t size) STUB_RETVAL(false)
+ bool ConnStateData::handleReadData(SBuf *buf) STUB_RETVAL(false)
  bool ConnStateData::handleRequestBodyData() STUB_RETVAL(false)
--void ConnStateData::pinConnection(const Comm::ConnectionPointer &pinServerConn, HttpRequest *request, CachePeer *peer, bool auth) STUB
 -void ConnStateData::unpinConnection() STUB
++void ConnStateData::pinConnection(const Comm::ConnectionPointer &pinServerConn, HttpRequest *request, CachePeer *peer, bool auth, bool monitor = true) STUB
 +void ConnStateData::unpinConnection(const bool andClose) STUB
  const Comm::ConnectionPointer ConnStateData::validatePinnedConnection(HttpRequest *request, const CachePeer *peer) STUB_RETVAL(NULL)
  void ConnStateData::clientPinnedConnectionClosed(const CommCloseCbParams &io) STUB
  void ConnStateData::clientReadRequest(const CommIoCbParams &io) STUB
diff --cc src/tools.cc
Simple merge