]> git.ipfire.org Git - thirdparty/squid.git/blobdiff - src/clients/FtpGateway.cc
Source Format Enforcement (#532)
[thirdparty/squid.git] / src / clients / FtpGateway.cc
index 9409ea186548447d32914eb5f3e0786a4afea606..79de14d7889ac005e3c1c8072247016e019c61ee 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 1996-2014 The Squid Software Foundation and contributors
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
  *
  * Squid software is distributed under GPLv2+ license and includes
  * contributions from numerous individuals and organizations.
@@ -10,6 +10,7 @@
 
 #include "squid.h"
 #include "acl/FilledChecklist.h"
+#include "base/PackableStream.h"
 #include "clients/forward.h"
 #include "clients/FtpClient.h"
 #include "comm.h"
@@ -27,9 +28,7 @@
 #include "HttpHeader.h"
 #include "HttpHeaderRange.h"
 #include "HttpReply.h"
-#include "HttpRequest.h"
 #include "ip/tools.h"
-#include "Mem.h"
 #include "MemBuf.h"
 #include "mime.h"
 #include "rfc1738.h"
@@ -39,7 +38,7 @@
 #include "StatCounters.h"
 #include "Store.h"
 #include "tools.h"
-#include "URL.h"
+#include "util.h"
 #include "wordlist.h"
 
 #if USE_DELAY_POOLS
@@ -118,7 +117,7 @@ public:
     String cwd_message;
     char *old_filepath;
     char typecode;
-    MemBuf listing;            ///< FTP directory listing in HTML format.
+    MemBuf listing;     ///< FTP directory listing in HTML format.
 
     GatewayFlags flags;
 
@@ -132,7 +131,7 @@ public:
     void unhack();
     void readStor();
     void parseListing();
-    MemBuf *htmlifyListEntry(const char *line);
+    bool htmlifyListEntry(const char *line, PackableStream &);
     void completedListing(void);
 
     /// create a data channel acceptor and start listening.
@@ -154,8 +153,8 @@ public:
     virtual void timeout(const CommTimeoutCbParams &io);
     void ftpAcceptDataConnection(const CommAcceptCbParams &io);
 
-    static HttpReply *ftpAuthRequired(HttpRequest * request, const char *realm);
-    const char *ftpRealm(void);
+    static HttpReply *ftpAuthRequired(HttpRequest * request, SBuf &realm, AccessLogEntry::Pointer &);
+    SBuf ftpRealm();
     void loginFailed(void);
 
     virtual void haveParsedReplyHeaders();
@@ -189,7 +188,7 @@ typedef struct {
     char *link;
 } ftpListParts;
 
-#define CTRL_BUFLEN 1024
+#define CTRL_BUFLEN 16*1024
 static char cbuf[CTRL_BUFLEN];
 
 /*
@@ -209,7 +208,9 @@ static FTPSM ftpSendMdtm;
 static FTPSM ftpReadMdtm;
 static FTPSM ftpSendSize;
 static FTPSM ftpReadSize;
+#if 0
 static FTPSM ftpSendEPRT;
+#endif
 static FTPSM ftpReadEPRT;
 static FTPSM ftpSendPORT;
 static FTPSM ftpReadPORT;
@@ -243,73 +244,73 @@ static FTPSM ftpReadQuit;
 /************************************************
 ** Debugs Levels used here                     **
 *************************************************
-0      CRITICAL Events
-1      IMPORTANT Events
-       Protocol and Transmission failures.
-2      FTP Protocol Chatter
-3      Logic Flows
-4      Data Parsing Flows
-5      Data Dumps
-7      ??
+0   CRITICAL Events
+1   IMPORTANT Events
+    Protocol and Transmission failures.
+2   FTP Protocol Chatter
+3   Logic Flows
+4   Data Parsing Flows
+5   Data Dumps
+7   ??
 ************************************************/
 
 /************************************************
 ** State Machine Description (excluding hacks) **
 *************************************************
-From                   To
+From            To
 ---------------------------------------
-Welcome                        User
-User                   Pass
-Pass                   Type
-Type                   TraverseDirectory / GetFile
-TraverseDirectory      Cwd / GetFile / ListDir
-Cwd                    TraverseDirectory / Mkdir
-GetFile                        Mdtm
-Mdtm                   Size
-Size                   Epsv
-ListDir                        Epsv
-Epsv                   FileOrList
-FileOrList             Rest / Retr / Nlst / List / Mkdir (PUT /xxx;type=d)
-Rest                   Retr
-Retr / Nlst / List     DataRead* (on datachannel)
-DataRead*              ReadTransferDone
-ReadTransferDone       DataTransferDone
-Stor                   DataWrite* (on datachannel)
-DataWrite*             RequestPutBody** (from client)
-RequestPutBody**       DataWrite* / WriteTransferDone
-WriteTransferDone      DataTransferDone
-DataTransferDone       Quit
-Quit                   -
+Welcome         User
+User            Pass
+Pass            Type
+Type            TraverseDirectory / GetFile
+TraverseDirectory   Cwd / GetFile / ListDir
+Cwd         TraverseDirectory / Mkdir
+GetFile         Mdtm
+Mdtm            Size
+Size            Epsv
+ListDir         Epsv
+Epsv            FileOrList
+FileOrList      Rest / Retr / Nlst / List / Mkdir (PUT /xxx;type=d)
+Rest            Retr
+Retr / Nlst / List  DataRead* (on datachannel)
+DataRead*       ReadTransferDone
+ReadTransferDone    DataTransferDone
+Stor            DataWrite* (on datachannel)
+DataWrite*      RequestPutBody** (from client)
+RequestPutBody**    DataWrite* / WriteTransferDone
+WriteTransferDone   DataTransferDone
+DataTransferDone    Quit
+Quit            -
 ************************************************/
 
 FTPSM *FTP_SM_FUNCS[] = {
-    ftpReadWelcome,            /* BEGIN */
-    ftpReadUser,               /* SENT_USER */
-    ftpReadPass,               /* SENT_PASS */
-    ftpReadType,               /* SENT_TYPE */
-    ftpReadMdtm,               /* SENT_MDTM */
-    ftpReadSize,               /* SENT_SIZE */
-    ftpReadEPRT,               /* SENT_EPRT */
-    ftpReadPORT,               /* SENT_PORT */
-    ftpReadEPSV,               /* SENT_EPSV_ALL */
-    ftpReadEPSV,               /* SENT_EPSV_1 */
-    ftpReadEPSV,               /* SENT_EPSV_2 */
-    ftpReadPasv,               /* SENT_PASV */
-    ftpReadCwd,                /* SENT_CWD */
-    ftpReadList,               /* SENT_LIST */
-    ftpReadList,               /* SENT_NLST */
-    ftpReadRest,               /* SENT_REST */
-    ftpReadRetr,               /* SENT_RETR */
-    ftpReadStor,               /* SENT_STOR */
-    ftpReadQuit,               /* SENT_QUIT */
-    ftpReadTransferDone,       /* READING_DATA (RETR,LIST,NLST) */
-    ftpWriteTransferDone,      /* WRITING_DATA (STOR) */
-    ftpReadMkdir,              /* SENT_MKDIR */
-    NULL,                      /* SENT_FEAT */
-    NULL,                      /* SENT_PWD */
-    NULL,                      /* SENT_CDUP*/
-    NULL,                      /* SENT_DATA_REQUEST */
-    NULL                       /* SENT_COMMAND */
+    ftpReadWelcome,     /* BEGIN */
+    ftpReadUser,        /* SENT_USER */
+    ftpReadPass,        /* SENT_PASS */
+    ftpReadType,        /* SENT_TYPE */
+    ftpReadMdtm,        /* SENT_MDTM */
+    ftpReadSize,        /* SENT_SIZE */
+    ftpReadEPRT,        /* SENT_EPRT */
+    ftpReadPORT,        /* SENT_PORT */
+    ftpReadEPSV,        /* SENT_EPSV_ALL */
+    ftpReadEPSV,        /* SENT_EPSV_1 */
+    ftpReadEPSV,        /* SENT_EPSV_2 */
+    ftpReadPasv,        /* SENT_PASV */
+    ftpReadCwd,     /* SENT_CWD */
+    ftpReadList,        /* SENT_LIST */
+    ftpReadList,        /* SENT_NLST */
+    ftpReadRest,        /* SENT_REST */
+    ftpReadRetr,        /* SENT_RETR */
+    ftpReadStor,        /* SENT_STOR */
+    ftpReadQuit,        /* SENT_QUIT */
+    ftpReadTransferDone,    /* READING_DATA (RETR,LIST,NLST) */
+    ftpWriteTransferDone,   /* WRITING_DATA (STOR) */
+    ftpReadMkdir,       /* SENT_MKDIR */
+    NULL,           /* SENT_FEAT */
+    NULL,           /* SENT_PWD */
+    NULL,           /* SENT_CDUP*/
+    NULL,           /* SENT_DATA_REQUEST */
+    NULL            /* SENT_COMMAND */
 };
 
 /// handler called by Comm when FTP data channel is closed unexpectedly
@@ -327,23 +328,23 @@ Ftp::Gateway::dataClosed(const CommCloseCbParams &io)
 }
 
 Ftp::Gateway::Gateway(FwdState *fwdState):
-        AsyncJob("FtpStateData"),
-        Ftp::Client(fwdState),
-        password_url(0),
-        reply_hdr(NULL),
-        reply_hdr_state(0),
-        conn_att(0),
-        login_att(0),
-        mdtm(-1),
-        theSize(-1),
-        pathcomps(NULL),
-        filepath(NULL),
-        dirpath(NULL),
-        restart_offset(0),
-        proxy_host(NULL),
-        list_width(0),
-        old_filepath(NULL),
-        typecode('\0')
+    AsyncJob("FtpStateData"),
+    Ftp::Client(fwdState),
+    password_url(0),
+    reply_hdr(NULL),
+    reply_hdr_state(0),
+    conn_att(0),
+    login_att(0),
+    mdtm(-1),
+    theSize(-1),
+    pathcomps(NULL),
+    filepath(NULL),
+    dirpath(NULL),
+    restart_offset(0),
+    proxy_host(NULL),
+    list_width(0),
+    old_filepath(NULL),
+    typecode('\0')
 {
     debugs(9, 3, entry->url());
 
@@ -443,6 +444,11 @@ Ftp::Gateway::loginParser(const SBuf &login, bool escaped)
 void
 Ftp::Gateway::listenForDataChannel(const Comm::ConnectionPointer &conn)
 {
+    if (!Comm::IsConnOpen(ctrl.conn)) {
+        debugs(9, 5, "The control connection to the remote end is closed");
+        return;
+    }
+
     assert(!Comm::IsConnOpen(data.conn));
 
     typedef CommCbMemFunT<Gateway, CommAcceptCbParams> AcceptDialer;
@@ -525,8 +531,10 @@ ftpListParseParts(const char *buf, struct Ftp::GatewayFlags flags)
 {
     ftpListParts *p = NULL;
     char *t = NULL;
-    const char *ct = NULL;
-    char *tokens[MAX_TOKENS];
+    struct FtpLineToken {
+        char *token = nullptr; ///< token image copied from the received line
+        size_t pos = 0;  ///< token offset on the received line
+    } tokens[MAX_TOKENS];
     int i;
     int n_tokens;
     static char tbuf[128];
@@ -567,7 +575,8 @@ ftpListParseParts(const char *buf, struct Ftp::GatewayFlags flags)
     }
 
     for (t = strtok(xbuf, w_space); t && n_tokens < MAX_TOKENS; t = strtok(NULL, w_space)) {
-        tokens[n_tokens] = xstrdup(t);
+        tokens[n_tokens].token = xstrdup(t);
+        tokens[n_tokens].pos = t - xbuf;
         ++n_tokens;
     }
 
@@ -575,10 +584,10 @@ ftpListParseParts(const char *buf, struct Ftp::GatewayFlags flags)
 
     /* locate the Month field */
     for (i = 3; i < n_tokens - 2; ++i) {
-        char *size = tokens[i - 1];
-        char *month = tokens[i];
-        char *day = tokens[i + 1];
-        char *year = tokens[i + 2];
+        const auto size = tokens[i - 1].token;
+        char *month = tokens[i].token;
+        char *day = tokens[i + 1].token;
+        char *year = tokens[i + 2].token;
 
         if (!is_month(month))
             continue;
@@ -589,33 +598,43 @@ ftpListParseParts(const char *buf, struct Ftp::GatewayFlags flags)
         if (regexec(&scan_ftp_integer, day, 0, NULL, 0) != 0)
             continue;
 
-        if (regexec(&scan_ftp_time, year, 0, NULL, 0) != 0)    /* Yr | hh:mm */
+        if (regexec(&scan_ftp_time, year, 0, NULL, 0) != 0) /* Yr | hh:mm */
             continue;
 
-        snprintf(tbuf, 128, "%s %2s %5s",
-                 month, day, year);
+        const auto *copyFrom = buf + tokens[i].pos;
 
-        if (!strstr(buf, tbuf))
-            snprintf(tbuf, 128, "%s %2s %-5s",
-                     month, day, year);
+        // "MMM DD [ YYYY|hh:mm]" with at most two spaces between DD and YYYY
+        auto dateSize = snprintf(tbuf, sizeof(tbuf), "%s %2s %5s", month, day, year);
+        bool isTypeA = (dateSize == 12) && (strncmp(copyFrom, tbuf, dateSize) == 0);
 
-        char const *copyFrom = NULL;
+        // "MMM DD [YYYY|hh:mm]" with one space between DD and YYYY
+        dateSize = snprintf(tbuf, sizeof(tbuf), "%s %2s %-5s", month, day, year);
+        bool isTypeB = (dateSize == 12 || dateSize == 11) && (strncmp(copyFrom, tbuf, dateSize) == 0);
 
-        if ((copyFrom = strstr(buf, tbuf))) {
-            p->type = *tokens[0];
+        // TODO: replace isTypeA and isTypeB with a regex.
+        if (isTypeA || isTypeB) {
+            p->type = *tokens[0].token;
             p->size = strtoll(size, NULL, 10);
+            const auto finalDateSize = snprintf(tbuf, sizeof(tbuf), "%s %2s %5s", month, day, year);
+            assert(finalDateSize >= 0);
             p->date = xstrdup(tbuf);
 
+            // point after tokens[i+2] :
+            copyFrom = buf + tokens[i + 2].pos + strlen(tokens[i + 2].token);
             if (flags.skip_whitespace) {
-                copyFrom += strlen(tbuf);
-
                 while (strchr(w_space, *copyFrom))
                     ++copyFrom;
             } else {
-                /* XXX assumes a single space between date and filename
+                /* Handle the following four formats:
+                 * "MMM DD  YYYY Name"
+                 * "MMM DD  YYYYName"
+                 * "MMM DD YYYY  Name"
+                 * "MMM DD YYYY Name"
+                 * Assuming a single space between date and filename
                  * suggested by:  Nathan.Bailey@cc.monash.edu.au and
                  * Mike Battersby <mike@starbug.bofh.asn.au> */
-                copyFrom += strlen(tbuf) + 1;
+                if (strchr(w_space, *copyFrom))
+                    ++copyFrom;
             }
 
             p->name = xstrdup(copyFrom);
@@ -633,45 +652,36 @@ ftpListParseParts(const char *buf, struct Ftp::GatewayFlags flags)
 
     /* try it as a DOS listing, 04-05-70 09:33PM ... */
     if (n_tokens > 3 &&
-            regexec(&scan_ftp_dosdate, tokens[0], 0, NULL, 0) == 0 &&
-            regexec(&scan_ftp_dostime, tokens[1], 0, NULL, 0) == 0) {
-        if (!strcasecmp(tokens[2], "<dir>")) {
+            regexec(&scan_ftp_dosdate, tokens[0].token, 0, NULL, 0) == 0 &&
+            regexec(&scan_ftp_dostime, tokens[1].token, 0, NULL, 0) == 0) {
+        if (!strcasecmp(tokens[2].token, "<dir>")) {
             p->type = 'd';
         } else {
             p->type = '-';
-            p->size = strtoll(tokens[2], NULL, 10);
+            p->size = strtoll(tokens[2].token, NULL, 10);
         }
 
-        snprintf(tbuf, 128, "%s %s", tokens[0], tokens[1]);
+        snprintf(tbuf, sizeof(tbuf), "%s %s", tokens[0].token, tokens[1].token);
         p->date = xstrdup(tbuf);
 
         if (p->type == 'd') {
-            /* Directory.. name begins with first printable after <dir> */
-            ct = strstr(buf, tokens[2]);
-            ct += strlen(tokens[2]);
-
-            while (xisspace(*ct))
-                ++ct;
-
-            if (!*ct)
-                ct = NULL;
+            // Directory.. name begins with first printable after <dir>
+            // Because of the "n_tokens > 3", the next printable after <dir>
+            // is stored at token[3]. No need for more checks here.
         } else {
-            /* A file. Name begins after size, with a space in between */
-            snprintf(tbuf, 128, " %s %s", tokens[2], tokens[3]);
-            ct = strstr(buf, tbuf);
-
-            if (ct) {
-                ct += strlen(tokens[2]) + 2;
-            }
+            // A file. Name begins after size, with a space in between.
+            // Also a space should exist before size.
+            // But there is not needed to be very strict with spaces.
+            // The name is stored at token[3], take it from here.
         }
 
-        p->name = xstrdup(ct ? ct : tokens[3]);
+        p->name = xstrdup(tokens[3].token);
         goto found;
     }
 
     /* Try EPLF format; carson@lehman.com */
     if (buf[0] == '+') {
-        ct = buf + 1;
+        const char *ct = buf + 1;
         p->type = 0;
 
         while (ct && *ct) {
@@ -696,7 +706,7 @@ ftpListParseParts(const char *buf, struct Ftp::GatewayFlags flags)
                 tm = (time_t) strtol(ct + 1, &tmp, 0);
 
                 if (tmp != ct + 1)
-                    break;     /* not a valid integer */
+                    break;  /* not a valid integer */
 
                 p->date = xstrdup(ctime(&tm));
 
@@ -742,61 +752,45 @@ blank:
 found:
 
     for (i = 0; i < n_tokens; ++i)
-        xfree(tokens[i]);
+        xfree(tokens[i].token);
 
     if (!p->name)
-        ftpListPartsFree(&p);  /* cleanup */
+        ftpListPartsFree(&p);   /* cleanup */
 
     return p;
 }
 
-MemBuf *
-Ftp::Gateway::htmlifyListEntry(const char *line)
+bool
+Ftp::Gateway::htmlifyListEntry(const char *line, PackableStream &html)
 {
-    char icon[2048];
-    char href[2048 + 40];
-    char text[ 2048];
-    char size[ 2048];
-    char chdir[ 2048 + 40];
-    char view[ 2048 + 40];
-    char download[ 2048 + 40];
-    char link[ 2048 + 40];
-    MemBuf *html;
-    char prefix[2048];
-    ftpListParts *parts;
-    *icon = *href = *text = *size = *chdir = *view = *download = *link = '\0';
-
-    debugs(9, 7, HERE << " line ={" << line << "}");
+    debugs(9, 7, "line={" << line << "}");
 
     if (strlen(line) > 1024) {
-        html = new MemBuf();
-        html->init();
-        html->Printf("<tr><td colspan=\"5\">%s</td></tr>\n", line);
-        return html;
+        html << "<tr><td colspan=\"5\">" << line << "</td></tr>\n";
+        return true;
     }
 
-    if (flags.dir_slash && dirpath && typecode != 'D')
-        snprintf(prefix, 2048, "%s/", rfc1738_escape_part(dirpath));
-    else
-        prefix[0] = '\0';
-
-    if ((parts = ftpListParseParts(line, flags)) == NULL) {
-        const char *p;
+    SBuf prefix;
+    if (flags.dir_slash && dirpath && typecode != 'D') {
+        prefix.append(rfc1738_escape_part(dirpath));
+        prefix.append("/", 1);
+    }
 
-        html = new MemBuf();
-        html->init();
-        html->Printf("<tr class=\"entry\"><td colspan=\"5\">%s</td></tr>\n", line);
+    ftpListParts *parts = ftpListParseParts(line, flags);
+    if (!parts) {
+        html << "<tr class=\"entry\"><td colspan=\"5\">" << line << "</td></tr>\n";
 
+        const char *p;
         for (p = line; *p && xisspace(*p); ++p);
         if (*p && !xisspace(*p))
             flags.listformat_unknown = 1;
 
-        return html;
+        return true;
     }
 
     if (!strcmp(parts->name, ".") || !strcmp(parts->name, "..")) {
         ftpListPartsFree(&parts);
-        return NULL;
+        return false;
     }
 
     parts->size += 1023;
@@ -804,98 +798,92 @@ Ftp::Gateway::htmlifyListEntry(const char *line)
     parts->showname = xstrdup(parts->name);
 
     /* {icon} {text} . . . {date}{size}{chdir}{view}{download}{link}\n  */
-    xstrncpy(href, rfc1738_escape_part(parts->name), 2048);
+    SBuf href(prefix);
+    href.append(rfc1738_escape_part(parts->name));
 
-    xstrncpy(text, parts->showname, 2048);
+    SBuf text(parts->showname);
 
+    SBuf icon, size, chdir, link;
     switch (parts->type) {
 
     case 'd':
-        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
-                 mimeGetIconURL("internal-dir"),
-                 "[DIR]");
-        strcat(href, "/");     /* margin is allocated above */
+        icon.appendf("<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
+                     mimeGetIconURL("internal-dir"),
+                     "[DIR]");
+        href.append("/", 1);  /* margin is allocated above */
         break;
 
     case 'l':
-        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
-                 mimeGetIconURL("internal-link"),
-                 "[LINK]");
+        icon.appendf("<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
+                     mimeGetIconURL("internal-link"),
+                     "[LINK]");
         /* sometimes there is an 'l' flag, but no "->" link */
 
         if (parts->link) {
-            char *link2 = xstrdup(html_quote(rfc1738_escape(parts->link)));
-            snprintf(link, 2048, " -&gt; <a href=\"%s%s\">%s</a>",
-                     *link2 != '/' ? prefix : "", link2,
-                     html_quote(parts->link));
-            safe_free(link2);
+            SBuf link2(html_quote(rfc1738_escape(parts->link)));
+            link.appendf(" -&gt; <a href=\"%s" SQUIDSBUFPH "\">%s</a>",
+                         link2[0] != '/' ? prefix.c_str() : "", SQUIDSBUFPRINT(link2),
+                         html_quote(parts->link));
         }
 
         break;
 
     case '\0':
-        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
-                 mimeGetIconURL(parts->name),
-                 "[UNKNOWN]");
-        snprintf(chdir, 2048, "<a href=\"%s/;type=d\"><img border=\"0\" src=\"%s\" "
-                 "alt=\"[DIR]\"></a>",
-                 rfc1738_escape_part(parts->name),
-                 mimeGetIconURL("internal-dir"));
+        icon.appendf("<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
+                     mimeGetIconURL(parts->name),
+                     "[UNKNOWN]");
+        chdir.appendf("<a href=\"%s/;type=d\"><img border=\"0\" src=\"%s\" "
+                      "alt=\"[DIR]\"></a>",
+                      rfc1738_escape_part(parts->name),
+                      mimeGetIconURL("internal-dir"));
         break;
 
     case '-':
 
     default:
-        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
-                 mimeGetIconURL(parts->name),
-                 "[FILE]");
-        snprintf(size, 2048, " %6" PRId64 "k", parts->size);
+        icon.appendf("<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
+                     mimeGetIconURL(parts->name),
+                     "[FILE]");
+        size.appendf(" %6" PRId64 "k", parts->size);
         break;
     }
 
+    SBuf view, download;
     if (parts->type != 'd') {
         if (mimeGetViewOption(parts->name)) {
-            snprintf(view, 2048, "<a href=\"%s%s;type=a\"><img border=\"0\" src=\"%s\" "
-                     "alt=\"[VIEW]\"></a>",
-                     prefix, href, mimeGetIconURL("internal-view"));
+            view.appendf("<a href=\"" SQUIDSBUFPH ";type=a\"><img border=\"0\" src=\"%s\" "
+                         "alt=\"[VIEW]\"></a>",
+                         SQUIDSBUFPRINT(href), mimeGetIconURL("internal-view"));
         }
 
         if (mimeGetDownloadOption(parts->name)) {
-            snprintf(download, 2048, "<a href=\"%s%s;type=i\"><img border=\"0\" src=\"%s\" "
-                     "alt=\"[DOWNLOAD]\"></a>",
-                     prefix, href, mimeGetIconURL("internal-download"));
+            download.appendf("<a href=\"" SQUIDSBUFPH ";type=i\"><img border=\"0\" src=\"%s\" "
+                             "alt=\"[DOWNLOAD]\"></a>",
+                             SQUIDSBUFPRINT(href), mimeGetIconURL("internal-download"));
         }
     }
 
     /* construct the table row from parts. */
-    html = new MemBuf();
-    html->init();
-    html->Printf("<tr class=\"entry\">"
-                 "<td class=\"icon\"><a href=\"%s%s\">%s</a></td>"
-                 "<td class=\"filename\"><a href=\"%s%s\">%s</a></td>"
-                 "<td class=\"date\">%s</td>"
-                 "<td class=\"size\">%s</td>"
-                 "<td class=\"actions\">%s%s%s%s</td>"
-                 "</tr>\n",
-                 prefix, href, icon,
-                 prefix, href, html_quote(text),
-                 parts->date,
-                 size,
-                 chdir, view, download, link);
+    html << "<tr class=\"entry\">"
+         "<td class=\"icon\"><a href=\"" << href << "\">" << icon << "</a></td>"
+         "<td class=\"filename\"><a href=\"" << href << "\">" << html_quote(text.c_str()) << "</a></td>"
+         "<td class=\"date\">" << parts->date << "</td>"
+         "<td class=\"size\">" << size << "</td>"
+         "<td class=\"actions\">" << chdir << view << download << link << "</td>"
+         "</tr>\n";
 
     ftpListPartsFree(&parts);
-    return html;
+    return true;
 }
 
 void
 Ftp::Gateway::parseListing()
 {
     char *buf = data.readBuf->content();
-    char *sbuf;                        /* NULL-terminated copy of termedBuf */
+    char *sbuf;         /* NULL-terminated copy of termedBuf */
     char *end;
     char *line;
     char *s;
-    MemBuf *t;
     size_t linelen;
     size_t usable;
     size_t len = data.readBuf->contentSize();
@@ -955,12 +943,14 @@ Ftp::Gateway::parseListing()
         if (!strncmp(line, "total", 5))
             continue;
 
-        t = htmlifyListEntry(line);
+        MemBuf htmlPage;
+        htmlPage.init();
+        PackableStream html(htmlPage);
 
-        if ( t != NULL) {
-            debugs(9, 7, HERE << "listing append: t = {" << t->contentSize() << ", '" << t->content() << "'}");
-            listing.append(t->content(), t->contentSize());
-//leak?            delete t;
+        if (htmlifyListEntry(line, html)) {
+            html.flush();
+            debugs(9, 7, "listing append: t = {" << htmlPage.contentSize() << ", '" << htmlPage.content() << "'}");
+            listing.append(htmlPage.content(), htmlPage.contentSize());
         }
     }
 
@@ -989,7 +979,7 @@ Ftp::Gateway::processReplyBody()
          * probably was aborted because content length exceeds one
          * of the maximum size limits.
          */
-        abortTransaction("entry aborted after calling appendSuccessHeader()");
+        abortAll("entry aborted after calling appendSuccessHeader()");
         return;
     }
 
@@ -1034,8 +1024,8 @@ Ftp::Gateway::processReplyBody()
  * TODO: we might be able to do something about locating username from other sources:
  *       ie, external ACL user=* tag or ident lookup
  *
- \retval 1     if we have everything needed to complete this request.
- \retval 0     if something is missing.
+ \retval 1  if we have everything needed to complete this request.
+ \retval 0  if something is missing.
  */
 int
 Ftp::Gateway::checkAuth(const HttpHeader * req_hdr)
@@ -1045,7 +1035,7 @@ Ftp::Gateway::checkAuth(const HttpHeader * req_hdr)
 
 #if HAVE_AUTH_MODULE_BASIC
     /* Check HTTP Authorization: headers (better than defaults, but less than URL) */
-    const SBuf auth(req_hdr->getAuth(HDR_AUTHORIZATION, "Basic"));
+    const auto auth(req_hdr->getAuthToken(Http::HdrType::AUTHORIZATION, "Basic"));
     if (!auth.isEmpty()) {
         flags.authenticated = 1;
         loginParser(auth, false);
@@ -1078,38 +1068,36 @@ Ftp::Gateway::checkAuth(const HttpHeader * req_hdr)
         }
     }
 
-    return 0;                  /* different username */
+    return 0;           /* different username */
 }
 
-static String str_type_eq;
 void
 Ftp::Gateway::checkUrlpath()
 {
-    int l;
-    size_t t;
+    static SBuf str_type_eq("type=");
+    auto t = request->url.path().rfind(';');
 
-    if (str_type_eq.size()==0) //hack. String doesn't support global-static
-        str_type_eq="type=";
-
-    if ((t = request->urlpath.rfind(';')) != String::npos) {
-        if (request->urlpath.substr(t+1,t+1+str_type_eq.size())==str_type_eq) {
-            typecode = (char)xtoupper(request->urlpath[t+str_type_eq.size()+1]);
-            request->urlpath.cut(t);
+    if (t != SBuf::npos) {
+        auto filenameEnd = t-1;
+        if (request->url.path().substr(++t).cmp(str_type_eq, str_type_eq.length()) == 0) {
+            t += str_type_eq.length();
+            typecode = (char)xtoupper(request->url.path()[t]);
+            request->url.path(request->url.path().substr(0,filenameEnd));
         }
     }
 
-    l = request->urlpath.size();
+    int l = request->url.path().length();
     /* check for null path */
 
     if (!l) {
         flags.isdir = 1;
         flags.root_dir = 1;
-        flags.need_base_href = 1;      /* Work around broken browsers */
-    } else if (!request->urlpath.cmp("/%2f/")) {
+        flags.need_base_href = 1;   /* Work around broken browsers */
+    } else if (!request->url.path().cmp("/%2f/")) {
         /* UNIX root directory */
         flags.isdir = 1;
         flags.root_dir = 1;
-    } else if ((l >= 1) && (request->urlpath[l - 1] == '/')) {
+    } else if ((l >= 1) && (request->url.path()[l-1] == '/')) {
         /* Directory URL, ending in / */
         flags.isdir = 1;
 
@@ -1130,14 +1118,10 @@ Ftp::Gateway::buildTitleUrl()
         title_url.append("@");
     }
 
-    title_url.append(request->GetHost());
-
-    if (request->port != urlDefaultPort(AnyP::PROTO_FTP)) {
-        title_url.append(":");
-        title_url.append(xitoa(request->port));
-    }
+    SBuf authority = request->url.authority(request->url.getScheme() != AnyP::PROTO_FTP);
 
-    title_url.append (request->urlpath);
+    title_url.append(authority.rawContent(), authority.length());
+    title_url.append(request->url.path().rawContent(), request->url.path().length());
 
     base_href = "ftp://";
 
@@ -1145,21 +1129,15 @@ Ftp::Gateway::buildTitleUrl()
         base_href.append(rfc1738_escape_part(user));
 
         if (password_url) {
-            base_href.append (":");
+            base_href.append(":");
             base_href.append(rfc1738_escape_part(password));
         }
 
         base_href.append("@");
     }
 
-    base_href.append(request->GetHost());
-
-    if (request->port != urlDefaultPort(AnyP::PROTO_FTP)) {
-        base_href.append(":");
-        base_href.append(xitoa(request->port));
-    }
-
-    base_href.append(request->urlpath);
+    base_href.append(authority.rawContent(), authority.length());
+    base_href.append(request->url.path().rawContent(), request->url.path().length());
     base_href.append("/");
 }
 
@@ -1168,7 +1146,8 @@ Ftp::Gateway::start()
 {
     if (!checkAuth(&request->header)) {
         /* create appropriate reply */
-        HttpReply *reply = ftpAuthRequired(request, ftpRealm());
+        SBuf realm(ftpRealm()); // local copy so SBuf will not disappear too early
+        const auto reply = ftpAuthRequired(request.getRaw(), realm, fwd->al);
         entry->replaceHttpReply(reply);
         serverComplete();
         return;
@@ -1176,8 +1155,8 @@ Ftp::Gateway::start()
 
     checkUrlpath();
     buildTitleUrl();
-    debugs(9, 5, HERE << "FD " << ctrl.conn->fd << " : host=" << request->GetHost() <<
-           ", path=" << request->urlpath << ", user=" << user << ", passwd=" << password);
+    debugs(9, 5, "FD " << (ctrl.conn ? ctrl.conn->fd : -1) << " : host=" << request->url.host() <<
+           ", path=" << request->url.path() << ", user=" << user << ", passwd=" << password);
     state = BEGIN;
     Ftp::Client::start();
 }
@@ -1239,59 +1218,39 @@ void
 Ftp::Gateway::loginFailed()
 {
     ErrorState *err = NULL;
-    const char *command, *reply;
 
     if ((state == SENT_USER || state == SENT_PASS) && ctrl.replycode >= 400) {
         if (ctrl.replycode == 421 || ctrl.replycode == 426) {
             // 421/426 - Service Overload - retry permitted.
-            err = new ErrorState(ERR_FTP_UNAVAILABLE, Http::scServiceUnavailable, fwd->request);
+            err = new ErrorState(ERR_FTP_UNAVAILABLE, Http::scServiceUnavailable, fwd->request, fwd->al);
         } else if (ctrl.replycode >= 430 && ctrl.replycode <= 439) {
             // 43x - Invalid or Credential Error - retry challenge required.
-            err = new ErrorState(ERR_FTP_FORBIDDEN, Http::scUnauthorized, fwd->request);
+            err = new ErrorState(ERR_FTP_FORBIDDEN, Http::scUnauthorized, fwd->request, fwd->al);
         } else if (ctrl.replycode >= 530 && ctrl.replycode <= 539) {
             // 53x - Credentials Missing - retry challenge required
             if (password_url) // but they were in the URI! major fail.
-                err = new ErrorState(ERR_FTP_FORBIDDEN, Http::scForbidden, fwd->request);
+                err = new ErrorState(ERR_FTP_FORBIDDEN, Http::scForbidden, fwd->request, fwd->al);
             else
-                err = new ErrorState(ERR_FTP_FORBIDDEN, Http::scUnauthorized, fwd->request);
+                err = new ErrorState(ERR_FTP_FORBIDDEN, Http::scUnauthorized, fwd->request, fwd->al);
         }
     }
 
-    // any other problems are general falures.
     if (!err) {
         ftpFail(this);
         return;
     }
 
-    err->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)
-        err->ftp.request = xstrdup(command);
-
-    if (reply)
-        err->ftp.reply = xstrdup(reply);
+    failed(ERR_NONE, ctrl.replycode, err);
+    // any other problems are general falures.
 
     HttpReply *newrep = err->BuildHttpReply();
     delete err;
 
 #if HAVE_AUTH_MODULE_BASIC
     /* add Authenticate header */
-    newrep->header.putAuth("Basic", ftpRealm());
+    // XXX: performance regression. c_str() may reallocate
+    SBuf realm(ftpRealm()); // local copy so SBuf will not disappear too early
+    newrep->header.putAuth("Basic", realm.c_str());
 #endif
 
     // add it to the store entry for response....
@@ -1299,18 +1258,19 @@ Ftp::Gateway::loginFailed()
     serverComplete();
 }
 
-const char *
+SBuf
 Ftp::Gateway::ftpRealm()
 {
-    static char realm[8192];
+    SBuf realm;
 
     /* This request is not fully authenticated */
-    if (!request) {
-        snprintf(realm, 8192, "FTP %s unknown", user);
-    } else if (request->port == 21) {
-        snprintf(realm, 8192, "FTP %s %s", user, request->GetHost());
-    } else {
-        snprintf(realm, 8192, "FTP %s %s port %d", user, request->GetHost(), request->port);
+    realm.appendf("FTP %s ", user);
+    if (!request)
+        realm.append("unknown", 7);
+    else {
+        realm.append(request->url.host());
+        if (request->url.port() != 21)
+            realm.appendf(" port %d", request->url.port());
     }
     return realm;
 }
@@ -1323,9 +1283,7 @@ ftpSendUser(Ftp::Gateway * ftpState)
         return;
 
     if (ftpState->proxy_host != NULL)
-        snprintf(cbuf, CTRL_BUFLEN, "USER %s@%s\r\n",
-                 ftpState->user,
-                 ftpState->request->GetHost());
+        snprintf(cbuf, CTRL_BUFLEN, "USER %s@%s\r\n", ftpState->user, ftpState->request->url.host());
     else
         snprintf(cbuf, CTRL_BUFLEN, "USER %s\r\n", ftpState->user);
 
@@ -1377,10 +1335,6 @@ ftpReadPass(Ftp::Gateway * ftpState)
 static void
 ftpSendType(Ftp::Gateway * ftpState)
 {
-    const char *t;
-    const char *filename;
-    char mode;
-
     /* check the server control channel is still available */
     if (!ftpState || !ftpState->haveControlChannel("ftpSendType"))
         return;
@@ -1388,7 +1342,7 @@ ftpSendType(Ftp::Gateway * ftpState)
     /*
      * Ref section 3.2.2 of RFC 1738
      */
-    mode = ftpState->typecode;
+    char mode = ftpState->typecode;
 
     switch (mode) {
 
@@ -1406,9 +1360,10 @@ ftpSendType(Ftp::Gateway * ftpState)
         if (ftpState->flags.isdir) {
             mode = 'A';
         } else {
-            t = ftpState->request->urlpath.rpos('/');
-            filename = t ? t + 1 : ftpState->request->urlpath.termedBuf();
-            mode = mimeGetTransferMode(filename);
+            auto t = ftpState->request->url.path().rfind('/');
+            // XXX: performance regression, c_str() may reallocate
+            SBuf filename = ftpState->request->url.path().substr(t != SBuf::npos ? t + 1 : 0);
+            mode = mimeGetTransferMode(filename.c_str());
         }
 
         break;
@@ -1435,7 +1390,7 @@ ftpReadType(Ftp::Gateway * ftpState)
     debugs(9, 3, HERE << "code=" << code);
 
     if (code == 200) {
-        p = path = xstrdup(ftpState->request->urlpath.termedBuf());
+        p = path = SBufToCstring(ftpState->request->url.path());
 
         if (*p == '/')
             ++p;
@@ -1469,7 +1424,6 @@ ftpReadType(Ftp::Gateway * ftpState)
 static void
 ftpTraverseDirectory(Ftp::Gateway * ftpState)
 {
-    wordlist *w;
     debugs(9, 4, HERE << (ftpState->filepath ? ftpState->filepath : "<NULL>"));
 
     safe_free(ftpState->dirpath);
@@ -1485,13 +1439,7 @@ ftpTraverseDirectory(Ftp::Gateway * ftpState)
     }
 
     /* Go to next path component */
-    w = ftpState->pathcomps;
-
-    ftpState->filepath = w->key;
-
-    ftpState->pathcomps = w->next;
-
-    delete w;
+    ftpState->filepath = wordlistChopHead(& ftpState->pathcomps);
 
     /* Check if we are to CWD or RETR */
     if (ftpState->pathcomps != NULL || ftpState->flags.isdir) {
@@ -1542,7 +1490,7 @@ ftpReadCwd(Ftp::Gateway * ftpState)
         /* Reset cwd_message to only include the last message */
         ftpState->cwd_message.reset("");
         for (wordlist *w = ftpState->ctrl.message; w; w = w->next) {
-            ftpState->cwd_message.append(' ');
+            ftpState->cwd_message.append('\n');
             ftpState->cwd_message.append(w->key);
         }
         ftpState->ctrl.message = NULL;
@@ -1583,9 +1531,9 @@ ftpReadMkdir(Ftp::Gateway * ftpState)
 
     debugs(9, 3, HERE << "path " << path << ", code " << code);
 
-    if (code == 257) {         /* success */
+    if (code == 257) {      /* success */
         ftpSendCwd(ftpState);
-    } else if (code == 550) {  /* dir exists */
+    } else if (code == 550) {   /* dir exists */
 
         if (ftpState->flags.put_mkdir) {
             ftpState->flags.put_mkdir = 1;
@@ -1743,7 +1691,7 @@ Ftp::Gateway::processHeadResponse()
      * trying to write to the client.
      */
     if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
-        abortTransaction("entry aborted while processing HEAD");
+        abortAll("entry aborted while processing HEAD");
         return;
     }
 
@@ -1765,7 +1713,9 @@ ftpReadPasv(Ftp::Gateway * ftpState)
     if (ftpState->handlePasvReply(srvAddr))
         ftpState->connectDataChannel();
     else {
-        ftpSendEPRT(ftpState);
+        ftpFail(ftpState);
+        // Currently disabled, does not work correctly:
+        // ftpSendEPRT(ftpState);
         return;
     }
 }
@@ -1781,7 +1731,7 @@ Ftp::Gateway::dataChannelConnected(const CommConnectCbParams &io)
 
         // ABORT on timeouts. server may be waiting on a broken TCP link.
         if (io.xerrno == Comm::TIMEOUT)
-            writeCommand("ABOR");
+            writeCommand("ABOR\r\n");
 
         // try another connection attempt with some other method
         ftpSendPassive(this);
@@ -1805,6 +1755,11 @@ ftpOpenListenSocket(Ftp::Gateway * ftpState, int fallback)
     }
     safe_free(ftpState->data.host);
 
+    if (!Comm::IsConnOpen(ftpState->ctrl.conn)) {
+        debugs(9, 5, "The control connection to the remote end is closed");
+        return;
+    }
+
     /*
      * Set up a listen socket on the same local address as the
      * control connection.
@@ -1818,7 +1773,13 @@ ftpOpenListenSocket(Ftp::Gateway * ftpState, int fallback)
      */
     if (fallback) {
         int on = 1;
-        setsockopt(ftpState->ctrl.conn->fd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on));
+        errno = 0;
+        if (setsockopt(ftpState->ctrl.conn->fd, SOL_SOCKET, SO_REUSEADDR,
+                       (char *) &on, sizeof(on)) == -1) {
+            int xerrno = errno;
+            // SO_REUSEADDR is only an optimization, no need to be verbose about error
+            debugs(9, 4, "setsockopt failed: " << xstrerr(xerrno));
+        }
         ftpState->ctrl.conn->flags |= COMM_REUSEADDR;
         temp->flags |= COMM_REUSEADDR;
     } else {
@@ -1890,9 +1851,14 @@ ftpReadPORT(Ftp::Gateway * ftpState)
     ftpRestOrList(ftpState);
 }
 
+#if 0
 static void
 ftpSendEPRT(Ftp::Gateway * ftpState)
 {
+    /* check the server control channel is still available */
+    if (!ftpState || !ftpState->haveControlChannel("ftpSendEPRT"))
+        return;
+
     if (Config.Ftp.epsv_all && ftpState->flags.epsv_all_sent) {
         debugs(9, DBG_IMPORTANT, "FTP does not allow EPRT method after 'EPSV ALL' has been sent.");
         return;
@@ -1928,6 +1894,7 @@ ftpSendEPRT(Ftp::Gateway * ftpState)
     ftpState->writeCommand(cbuf);
     ftpState->state = Ftp::Client::SENT_EPRT;
 }
+#endif
 
 static void
 ftpReadEPRT(Ftp::Gateway * ftpState)
@@ -1954,10 +1921,8 @@ Ftp::Gateway::ftpAcceptDataConnection(const CommAcceptCbParams &io)
 {
     debugs(9, 3, HERE);
 
-    if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
-        abortTransaction("entry aborted when accepting data conn");
-        data.listenConn->close();
-        data.listenConn = NULL;
+    if (!Comm::IsConnOpen(ctrl.conn)) { /*Close handlers will cleanup*/
+        debugs(9, 5, "The control connection to the remote end is closed");
         return;
     }
 
@@ -1970,6 +1935,14 @@ Ftp::Gateway::ftpAcceptDataConnection(const CommAcceptCbParams &io)
         return;
     }
 
+    if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
+        abortAll("entry aborted when accepting data conn");
+        data.listenConn->close();
+        data.listenConn = NULL;
+        io.conn->close();
+        return;
+    }
+
     /* data listening conn is no longer even open. abort. */
     if (!Comm::IsConnOpen(data.listenConn)) {
         data.listenConn = NULL; // ensure that it's cleared and not just closed.
@@ -2029,9 +2002,9 @@ ftpRestOrList(Ftp::Gateway * ftpState)
         ftpState->flags.isdir = 1;
 
         if (ftpState->flags.put) {
-            ftpSendMkdir(ftpState);    /* PUT name;type=d */
+            ftpSendMkdir(ftpState); /* PUT name;type=d */
         } else {
-            ftpSendNlst(ftpState);     /* GET name;type=d  sec 3.2.2 of RFC 1738 */
+            ftpSendNlst(ftpState);  /* GET name;type=d  sec 3.2.2 of RFC 1738 */
         }
     } else if (ftpState->flags.put) {
         ftpSendStor(ftpState);
@@ -2057,7 +2030,7 @@ ftpSendStor(Ftp::Gateway * ftpState)
         snprintf(cbuf, CTRL_BUFLEN, "STOR %s\r\n", ftpState->filepath);
         ftpState->writeCommand(cbuf);
         ftpState->state = Ftp::Client::SENT_STOR;
-    } else if (ftpState->request->header.getInt64(HDR_CONTENT_LENGTH) > 0) {
+    } else if (ftpState->request->header.getInt64(Http::HdrType::CONTENT_LENGTH) > 0) {
         /* File upload without a filename. use STOU to generate one */
         snprintf(cbuf, CTRL_BUFLEN, "STOU\r\n");
         ftpState->writeCommand(cbuf);
@@ -2097,7 +2070,7 @@ void Ftp::Gateway::readStor()
         debugs(9, 3, HERE << "starting data transfer");
         switchTimeoutToDataChannel();
         sendMoreRequestBody();
-        fwd->dontRetry(true); // dont permit re-trying if the body was sent.
+        fwd->dontRetry(true); // do not permit re-trying if the body was sent.
         state = WRITING_DATA;
         debugs(9, 3, HERE << "writing data channel");
     } else if (code == 150) {
@@ -2287,13 +2260,12 @@ Ftp::Gateway::completedListing()
 {
     assert(entry);
     entry->lock("Ftp::Gateway");
-    ErrorState ferr(ERR_DIR_LISTING, Http::scOkay, request);
+    ErrorState ferr(ERR_DIR_LISTING, Http::scOkay, request.getRaw(), fwd->al);
     ferr.ftp.listing = &listing;
     ferr.ftp.cwd_msg = xstrdup(cwd_message.size()? cwd_message.termedBuf() : "");
     ferr.ftp.server_msg = ctrl.message;
     ctrl.message = NULL;
-    entry->replaceHttpReply( ferr.BuildHttpReply() );
-    EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT);
+    entry->replaceHttpReply(ferr.BuildHttpReply());
     entry->flush();
     entry->unlock("Ftp::Gateway");
 }
@@ -2311,7 +2283,7 @@ ftpReadTransferDone(Ftp::Gateway * ftpState)
             /* QUIT operation handles sending the reply to client */
         }
         ftpSendQuit(ftpState);
-    } else {                   /* != 226 */
+    } else {            /* != 226 */
         debugs(9, DBG_IMPORTANT, HERE << "Got code " << code << " after reading data");
         ftpState->failed(ERR_FTP_FAILURE, 0);
         /* failed closes ctrl.conn and frees ftpState */
@@ -2340,7 +2312,7 @@ ftpWriteTransferDone(Ftp::Gateway * ftpState)
         return;
     }
 
-    ftpState->entry->timestampsSet();  /* XXX Is this needed? */
+    ftpState->entry->timestampsSet();   /* XXX Is this needed? */
     ftpSendReply(ftpState);
 }
 
@@ -2380,7 +2352,7 @@ ftpTrySlashHack(Ftp::Gateway * ftpState)
     safe_free(ftpState->filepath);
 
     /* Build the new path (urlpath begins with /) */
-    path = xstrdup(ftpState->request->urlpath.termedBuf());
+    path = SBufToCstring(ftpState->request->url.path());
 
     rfc1738_unescape(path);
 
@@ -2431,17 +2403,21 @@ Ftp::Gateway::hackShortcut(FTPSM * nextState)
 static void
 ftpFail(Ftp::Gateway *ftpState)
 {
-    debugs(9, 6, HERE << "flags(" <<
+    const bool slashHack = ftpState->request->url.path().caseCmp("/%2f", 4)==0;
+    int code = ftpState->ctrl.replycode;
+    err_type error_code = ERR_NONE;
+
+    debugs(9, 6, "state " << ftpState->state <<
+           " reply code " << code << "flags(" <<
            (ftpState->flags.isdir?"IS_DIR,":"") <<
            (ftpState->flags.try_slash_hack?"TRY_SLASH_HACK":"") << "), " <<
            "mdtm=" << ftpState->mdtm << ", size=" << ftpState->theSize <<
-           "slashhack=" << (ftpState->request->urlpath.caseCmp("/%2f", 4)==0? "T":"F") );
+           "slashhack=" << (slashHack? "T":"F"));
 
     /* Try the / hack to support "Netscape" FTP URL's for retreiving files */
-    if (!ftpState->flags.isdir &&      /* Not a directory */
-            !ftpState->flags.try_slash_hack && /* Not in slash hack */
-            ftpState->mdtm <= 0 && ftpState->theSize < 0 &&    /* Not known as a file */
-            ftpState->request->urlpath.caseCmp("/%2f", 4) != 0) {      /* No slash encoded */
+    if (!ftpState->flags.isdir &&   /* Not a directory */
+            !ftpState->flags.try_slash_hack && !slashHack && /* Not doing slash hack */
+            ftpState->mdtm <= 0 && ftpState->theSize < 0) { /* Not known as a file */
 
         switch (ftpState->state) {
 
@@ -2457,8 +2433,15 @@ ftpFail(Ftp::Gateway *ftpState)
         }
     }
 
-    ftpState->failed(ERR_NONE, 0);
-    /* failed() closes ctrl.conn and frees this */
+    Http::StatusCode sc = ftpState->failedHttpStatus(error_code);
+    const auto ftperr = new ErrorState(error_code, sc, ftpState->fwd->request, ftpState->fwd->al);
+    ftpState->failed(error_code, code, ftperr);
+    ftperr->detailError(code);
+    HttpReply *newrep = ftperr->BuildHttpReply();
+    delete ftperr;
+
+    ftpState->entry->replaceHttpReply(newrep);
+    ftpSendQuit(ftpState);
 }
 
 Http::StatusCode
@@ -2519,7 +2502,7 @@ ftpSendReply(Ftp::Gateway * ftpState)
         http_code = Http::scInternalServerError;
     }
 
-    ErrorState err(err_code, http_code, ftpState->request);
+    ErrorState err(err_code, http_code, ftpState->request.getRaw(), ftpState->fwd->al);
 
     if (ftpState->old_request)
         err.ftp.request = xstrdup(ftpState->old_request);
@@ -2536,7 +2519,7 @@ ftpSendReply(Ftp::Gateway * ftpState)
     // TODO: interpret as FTP-specific error code
     err.detailError(code);
 
-    ftpState->entry->replaceHttpReply( err.BuildHttpReply() );
+    ftpState->entry->replaceHttpReply(err.BuildHttpReply());
 
     ftpSendQuit(ftpState);
 }
@@ -2544,12 +2527,6 @@ ftpSendReply(Ftp::Gateway * ftpState)
 void
 Ftp::Gateway::appendSuccessHeader()
 {
-    const char *mime_type = NULL;
-    const char *mime_enc = NULL;
-    String urlpath = request->urlpath;
-    const char *filename = NULL;
-    const char *t = NULL;
-
     debugs(9, 3, HERE);
 
     if (flags.http_header_sent)
@@ -2561,11 +2538,14 @@ Ftp::Gateway::appendSuccessHeader()
 
     assert(entry->isEmpty());
 
-    EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT);
+    entry->buffer();    /* released when done processing current data payload */
 
-    entry->buffer();   /* released when done processing current data payload */
+    SBuf urlPath = request->url.path();
+    auto t = urlPath.rfind('/');
+    SBuf filename = urlPath.substr(t != SBuf::npos ? t : 0);
 
-    filename = (t = urlpath.rpos('/')) ? t + 1 : urlpath.termedBuf();
+    const char *mime_type = NULL;
+    const char *mime_enc = NULL;
 
     if (flags.isdir) {
         mime_type = "text/html";
@@ -2574,7 +2554,8 @@ Ftp::Gateway::appendSuccessHeader()
 
         case 'I':
             mime_type = "application/octet-stream";
-            mime_enc = mimeGetContentEncoding(filename);
+            // XXX: performance regression, c_str() may reallocate
+            mime_enc = mimeGetContentEncoding(filename.c_str());
             break;
 
         case 'A':
@@ -2582,8 +2563,9 @@ Ftp::Gateway::appendSuccessHeader()
             break;
 
         default:
-            mime_type = mimeGetContentType(filename);
-            mime_enc = mimeGetContentEncoding(filename);
+            // XXX: performance regression, c_str() may reallocate
+            mime_type = mimeGetContentType(filename.c_str());
+            mime_enc = mimeGetContentEncoding(filename.c_str());
             break;
         }
     }
@@ -2616,8 +2598,9 @@ Ftp::Gateway::appendSuccessHeader()
 
     /* additional info */
     if (mime_enc)
-        reply->header.putStr(HDR_CONTENT_ENCODING, mime_enc);
+        reply->header.putStr(Http::HdrType::CONTENT_ENCODING, mime_enc);
 
+    reply->sources |= Http::Message::srcFtp;
     setVirginReply(reply);
     adaptOrFinalizeReply();
 }
@@ -2631,49 +2614,46 @@ Ftp::Gateway::haveParsedReplyHeaders()
 
     e->timestampsSet();
 
-    if (flags.authenticated) {
-        /*
-         * Authenticated requests can't be cached.
-         */
-        e->release();
-    } else if (!EBIT_TEST(e->flags, RELEASE_REQUEST) && !getCurrentOffset()) {
-        e->setPublicKey();
-    } else {
+    // makePublic() if allowed/possible or release() otherwise
+    if (flags.authenticated || // authenticated requests can't be cached
+            getCurrentOffset() ||
+            !e->makePublic()) {
         e->release();
     }
 }
 
 HttpReply *
-Ftp::Gateway::ftpAuthRequired(HttpRequest * request, const char *realm)
+Ftp::Gateway::ftpAuthRequired(HttpRequest * request, SBuf &realm, AccessLogEntry::Pointer &ale)
 {
-    ErrorState err(ERR_CACHE_ACCESS_DENIED, Http::scUnauthorized, request);
+    ErrorState err(ERR_CACHE_ACCESS_DENIED, Http::scUnauthorized, request, ale);
     HttpReply *newrep = err.BuildHttpReply();
 #if HAVE_AUTH_MODULE_BASIC
     /* add Authenticate header */
-    newrep->header.putAuth("Basic", realm);
+    // XXX: performance regression. c_str() may reallocate
+    newrep->header.putAuth("Basic", realm.c_str());
 #endif
     return newrep;
 }
 
-const char *
+const SBuf &
 Ftp::UrlWith2f(HttpRequest * request)
 {
-    String newbuf = "%2f";
+    SBuf newbuf("%2f");
 
-    if (request->url.getScheme() != AnyP::PROTO_FTP)
-        return NULL;
+    if (request->url.getScheme() != AnyP::PROTO_FTP) {
+        static const SBuf nil;
+        return nil;
+    }
 
-    if ( request->urlpath[0]=='/' ) {
-        newbuf.append(request->urlpath);
-        request->urlpath.absorb(newbuf);
-        safe_free(request->canonical);
-    } else if ( !strncmp(request->urlpath.termedBuf(), "%2f", 3) ) {
-        newbuf.append(request->urlpath.substr(1,request->urlpath.size()));
-        request->urlpath.absorb(newbuf);
-        safe_free(request->canonical);
+    if (request->url.path()[0] == '/') {
+        newbuf.append(request->url.path());
+        request->url.path(newbuf);
+    } else if (!request->url.path().startsWith(newbuf)) {
+        newbuf.append(request->url.path().substr(1));
+        request->url.path(newbuf);
     }
 
-    return urlCanonical(request);
+    return request->effectiveRequestUri();
 }
 
 void
@@ -2703,14 +2683,14 @@ Ftp::Gateway::writeReplyBody(const char *dataToWrite, size_t dataLength)
  * A hack to ensure we do not double-complete on the forward entry.
  *
  \todo Ftp::Gateway logic should probably be rewritten to avoid
- *     double-completion or FwdState should be rewritten to allow it.
+ *  double-completion or FwdState should be rewritten to allow it.
  */
 void
 Ftp::Gateway::completeForwarding()
 {
     if (fwd == NULL || flags.completed_forwarding) {
-        debugs(9, 3, HERE << "completeForwarding avoids " <<
-               "double-complete on FD " << ctrl.conn->fd << ", Data FD " << data.conn->fd <<
+        debugs(9, 3, "avoid double-complete on FD " <<
+               (ctrl.conn ? ctrl.conn->fd : -1) << ", Data FD " << data.conn->fd <<
                ", this " << this << ", fwd " << fwd);
         return;
     }
@@ -2722,8 +2702,8 @@ Ftp::Gateway::completeForwarding()
 /**
  * Have we lost the FTP server control channel?
  *
- \retval true  The server control channel is available.
- \retval false The server control channel is not available.
+ \retval true   The server control channel is available.
+ \retval false  The server control channel is not available.
  */
 bool
 Ftp::Gateway::haveControlChannel(const char *caller_name) const
@@ -2753,3 +2733,4 @@ Ftp::StartGateway(FwdState *const fwdState)
 {
     return AsyncJob::Start(new Ftp::Gateway(fwdState));
 }
+