/* * $Id$ * * DEBUG: section 09 File Transfer Protocol (FTP) * AUTHOR: Harvest Derived * * SQUID Web Proxy Cache http://www.squid-cache.org/ * ---------------------------------------------------------- * * Squid is the result of efforts by numerous individuals from * the Internet community; see the CONTRIBUTORS file for full * details. Many organizations have provided support for Squid's * development; see the SPONSORS file for full details. Squid is * Copyrighted (C) 2001 by the Regents of the University of * California; see the COPYRIGHT file for full details. Squid * incorporates software developed and/or copyrighted by other * sources; see the CREDITS file for full details. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. * */ #include "squid.h" #include "Store.h" #include "HttpRequest.h" #include "HttpReply.h" #include "errorpage.h" #include "fde.h" #include "comm.h" #include "HttpHeaderRange.h" #include "HttpHdrContRange.h" #include "HttpHeader.h" #if DELAY_POOLS #include "DelayPools.h" #include "MemObject.h" #endif #include "ConnectionDetail.h" #include "forward.h" #include "Server.h" #include "ip/tools.h" #include "MemBuf.h" #include "wordlist.h" #include "SquidTime.h" #include "URLScheme.h" #include "rfc1738.h" /** \defgroup ServerProtocolFTPInternal Server-Side FTP Internals \ingroup ServerProtocolFTPAPI */ /// \ingroup ServerProtocolFTPInternal static const char *const crlf = "\r\n"; /// \ingroup ServerProtocolFTPInternal static char cbuf[1024]; /// \ingroup ServerProtocolFTPInternal typedef enum { BEGIN, SENT_USER, SENT_PASS, SENT_TYPE, SENT_MDTM, SENT_SIZE, SENT_EPRT, SENT_PORT, SENT_EPSV_ALL, SENT_EPSV_1, SENT_EPSV_2, SENT_PASV, SENT_CWD, SENT_LIST, SENT_NLST, SENT_REST, SENT_RETR, SENT_STOR, SENT_QUIT, READING_DATA, WRITING_DATA, SENT_MKDIR } ftp_state_t; /// \ingroup ServerProtocolFTPInternal struct _ftp_flags { /* passive mode */ bool pasv_supported; ///< PASV command is allowed bool epsv_all_sent; ///< EPSV ALL has been used. Must abort on failures. bool pasv_only; /* authentication */ bool authenticated; ///< authentication success bool tried_auth_anonymous; ///< auth has tried to use anonymous credentials already. bool tried_auth_nopass; ///< auth tried username with no password already. /* other */ bool isdir; bool skip_whitespace; bool rest_supported; bool http_header_sent; bool tried_nlst; bool need_base_href; bool dir_slash; bool root_dir; bool no_dotdot; bool html_header_sent; bool binary; bool try_slash_hack; bool put; bool put_mkdir; bool listformat_unknown; bool listing_started; bool completed_forwarding; }; class FtpStateData; /// \ingroup ServerProtocolFTPInternal typedef void (FTPSM) (FtpStateData *); /// common code for FTP control and data channels // does not own the channel descriptor, which is managed by FtpStateData class FtpChannel { public: FtpChannel(): fd(-1) {} /// called after the socket is opened, sets up close handler void opened(int aFd, const AsyncCall::Pointer &aCloser); void close(); /// clears the close handler and calls comm_close void clear(); /// just resets fd and close handler int fd; /// channel descriptor; \todo: remove because the closer has it private: AsyncCall::Pointer closer; /// Comm close handler callback }; /// \ingroup ServerProtocolFTPInternal class FtpStateData : public ServerStateData { public: void *operator new (size_t); void operator delete (void *); void *toCbdata() { return this; } FtpStateData(FwdState *); ~FtpStateData(); char user[MAX_URL]; char password[MAX_URL]; int password_url; char *reply_hdr; int reply_hdr_state; String clean_url; String title_url; String base_href; int conn_att; int login_att; ftp_state_t state; time_t mdtm; int64_t theSize; wordlist *pathcomps; char *filepath; char *dirpath; int64_t restart_offset; char *proxy_host; size_t list_width; wordlist *cwd_message; char *old_request; char *old_reply; char *old_filepath; char typecode; // \todo: optimize ctrl and data structs member order, to minimize size /// FTP control channel info; the channel is opened once per transaction struct CtrlChannel: public FtpChannel { char *buf; size_t size; size_t offset; wordlist *message; char *last_command; char *last_reply; int replycode; } ctrl; /// FTP data channel info; the channel may be opened/closed a few times struct DataChannel: public FtpChannel { MemBuf *readBuf; char *host; unsigned short port; bool read_pending; } data; struct _ftp_flags flags; private: CBDATA_CLASS(FtpStateData); public: // these should all be private void start(); void loginParser(const char *, int escaped); int restartable(); void appendSuccessHeader(); void hackShortcut(FTPSM * nextState); void failed(err_type, int xerrno); void failedErrorMessage(err_type, int xerrno); void unhack(); void listingStart(); void listingFinish(); void scheduleReadControlReply(int); void handleControlReply(); void readStor(); char *htmlifyListEntry(const char *line); void parseListing(); void dataComplete(); void dataRead(const CommIoCbParams &io); int checkAuth(const HttpHeader * req_hdr); void checkUrlpath(); void buildTitleUrl(); void writeReplyBody(const char *, size_t len); void printfReplyBody(const char *fmt, ...); virtual int dataDescriptor() const; virtual void maybeReadVirginBody(); virtual void closeServer(); virtual void completeForwarding(); virtual void abortTransaction(const char *reason); void processHeadResponse(); void processReplyBody(); void writeCommand(const char *buf); void setCurrentOffset(int64_t offset) { currentOffset = offset; } int64_t getCurrentOffset() const { return currentOffset; } static CNCB ftpPasvCallback; static PF ftpDataWrite; void ftpTimeout(const CommTimeoutCbParams &io); void ctrlClosed(const CommCloseCbParams &io); void dataClosed(const CommCloseCbParams &io); void ftpReadControlReply(const CommIoCbParams &io); void ftpWriteCommandCallback(const CommIoCbParams &io); void ftpAcceptDataConnection(const CommAcceptCbParams &io); static HttpReply *ftpAuthRequired(HttpRequest * request, const char *realm); const char *ftpRealm(void); void loginFailed(void); static wordlist *ftpParseControlReply(char *, size_t, int *, size_t *); // sending of the request body to the server virtual void sentRequestBody(const CommIoCbParams&); virtual void doneSendingRequestBody(); virtual void haveParsedReplyHeaders(); virtual bool doneWithServer() const; virtual bool haveControlChannel(const char *caller_name) const; AsyncCall::Pointer dataCloser(); /// creates a Comm close callback private: // BodyConsumer for HTTP: consume request body. virtual void handleRequestBodyProducerAborted(); }; CBDATA_CLASS_INIT(FtpStateData); void * FtpStateData::operator new (size_t) { CBDATA_INIT_TYPE(FtpStateData); FtpStateData *result = cbdataAlloc(FtpStateData); return result; } void FtpStateData::operator delete (void *address) { FtpStateData *t = static_cast(address); cbdataFree(t); } /// \ingroup ServerProtocolFTPInternal typedef struct { char type; int64_t size; char *date; char *name; char *showname; char *link; } ftpListParts; /// \ingroup ServerProtocolFTPInternal #define FTP_LOGIN_ESCAPED 1 /// \ingroup ServerProtocolFTPInternal #define FTP_LOGIN_NOT_ESCAPED 0 /* * State machine functions * send == state transition * read == wait for response, and select next state transition * other == Transition logic */ static FTPSM ftpReadWelcome; static FTPSM ftpSendUser; static FTPSM ftpReadUser; static FTPSM ftpSendPass; static FTPSM ftpReadPass; static FTPSM ftpSendType; static FTPSM ftpReadType; static FTPSM ftpSendMdtm; static FTPSM ftpReadMdtm; static FTPSM ftpSendSize; static FTPSM ftpReadSize; static FTPSM ftpSendEPRT; static FTPSM ftpReadEPRT; static FTPSM ftpSendPORT; static FTPSM ftpReadPORT; static FTPSM ftpSendPassive; static FTPSM ftpReadEPSV; static FTPSM ftpReadPasv; static FTPSM ftpTraverseDirectory; static FTPSM ftpListDir; static FTPSM ftpGetFile; static FTPSM ftpSendCwd; static FTPSM ftpReadCwd; static FTPSM ftpRestOrList; static FTPSM ftpSendList; static FTPSM ftpSendNlst; static FTPSM ftpReadList; static FTPSM ftpSendRest; static FTPSM ftpReadRest; static FTPSM ftpSendRetr; static FTPSM ftpReadRetr; static FTPSM ftpReadTransferDone; static FTPSM ftpSendStor; static FTPSM ftpReadStor; static FTPSM ftpWriteTransferDone; static FTPSM ftpSendReply; static FTPSM ftpSendMkdir; static FTPSM ftpReadMkdir; static FTPSM ftpFail; static FTPSM ftpSendQuit; 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 ?? ************************************************/ /************************************************ ** State Machine Description (excluding hacks) ** ************************************************* 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 - ************************************************/ /// \ingroup ServerProtocolFTPInternal 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 */ }; /// handler called by Comm when FTP control channel is closed unexpectedly void FtpStateData::ctrlClosed(const CommCloseCbParams &io) { ctrl.clear(); deleteThis("FtpStateData::ctrlClosed"); } /// handler called by Comm when FTP data channel is closed unexpectedly void FtpStateData::dataClosed(const CommCloseCbParams &io) { data.clear(); failed(ERR_FTP_FAILURE, 0); /* failed closes ctrl.fd and frees ftpState */ /* NP: failure recovery may be possible when its only a data.fd failure. * is the ctrl.fd is still fine, we can send ABOR down it and retry. * Just need to watch out for wider Squid states like shutting down or reconfigure. */ } FtpStateData::FtpStateData(FwdState *theFwdState) : AsyncJob("FtpStateData"), ServerStateData(theFwdState) { const char *url = entry->url(); debugs(9, 3, HERE << "'" << url << "'" ); statCounter.server.all.requests++; statCounter.server.ftp.requests++; theSize = -1; mdtm = -1; if (Config.Ftp.passive && !theFwdState->ftpPasvFailed()) flags.pasv_supported = 1; flags.rest_supported = 1; typedef CommCbMemFunT Dialer; AsyncCall::Pointer closer = JobCallback(9, 5, Dialer, this, FtpStateData::ctrlClosed); ctrl.opened(theFwdState->server_fd, closer); if (request->method == METHOD_PUT) flags.put = 1; } FtpStateData::~FtpStateData() { debugs(9, 3, HERE << entry->url() ); if (reply_hdr) { memFree(reply_hdr, MEM_8K_BUF); reply_hdr = NULL; } data.close(); if (ctrl.fd >= 0) { debugs(9, DBG_IMPORTANT, HERE << "Internal bug: FtpStateData left " << "control FD " << ctrl.fd << " open"); } if (ctrl.buf) { memFreeBuf(ctrl.size, ctrl.buf); ctrl.buf = NULL; } if (data.readBuf) { if (!data.readBuf->isNull()) data.readBuf->clean(); delete data.readBuf; } if (pathcomps) wordlistDestroy(&pathcomps); if (ctrl.message) wordlistDestroy(&ctrl.message); if (cwd_message) wordlistDestroy(&cwd_message); safe_free(ctrl.last_reply); safe_free(ctrl.last_command); safe_free(old_request); safe_free(old_reply); safe_free(old_filepath); title_url.clean(); base_href.clean(); safe_free(filepath); safe_free(dirpath); safe_free(data.host); fwd = NULL; // refcounted } /** * Parse a possible login username:password pair. * Produces filled member variables user, password, password_url if anything found. */ void FtpStateData::loginParser(const char *login, int escaped) { const char *u = NULL; // end of the username sub-string int len; // length of the current sub-string to handle. int total_len = strlen(login); debugs(9, 4, HERE << ": login='" << login << "', escaped=" << escaped); debugs(9, 9, HERE << ": IN : login='" << login << "', escaped=" << escaped << ", user=" << user << ", password=" << password); if ((u = strchr(login, ':'))) { /* if there was a username part */ if (u > login) { len = u - login; ++u; // jump off the delimiter. if (len > MAX_URL) len = MAX_URL-1; xstrncpy(user, login, len +1); debugs(9, 9, HERE << ": found user='" << user << "'(" << len <<"), escaped=" << escaped); if (escaped) rfc1738_unescape(user); debugs(9, 9, HERE << ": found user='" << user << "'(" << len <<") unescaped."); } /* if there was a password part */ len = login + total_len - u; if ( len > 0) { if (len > MAX_URL) len = MAX_URL -1; xstrncpy(password, u, len +1); debugs(9, 9, HERE << ": found password='" << password << "'(" << len <<"), escaped=" << escaped); if (escaped) { rfc1738_unescape(password); password_url = 1; } debugs(9, 9, HERE << ": found password='" << password << "'(" << len <<") unescaped."); } } else if (login[0]) { /* no password, just username */ if (total_len > MAX_URL) total_len = MAX_URL -1; xstrncpy(user, login, total_len +1); debugs(9, 9, HERE << ": found user='" << user << "'(" << total_len <<"), escaped=" << escaped); if (escaped) rfc1738_unescape(user); debugs(9, 9, HERE << ": found user='" << user << "'(" << total_len <<") unescaped."); } debugs(9, 9, HERE << ": OUT: login='" << login << "', escaped=" << escaped << ", user=" << user << ", password=" << password); } void FtpStateData::ftpTimeout(const CommTimeoutCbParams &io) { debugs(9, 4, "ftpTimeout: FD " << io.fd << ": '" << entry->url() << "'" ); if (SENT_PASV == state && io.fd == data.fd) { /* stupid ftp.netscape.com */ fwd->dontRetry(false); fwd->ftpPasvFailed(true); debugs(9, DBG_IMPORTANT, "ftpTimeout: timeout in SENT_PASV state" ); } failed(ERR_READ_TIMEOUT, 0); /* failed() closes ctrl.fd and frees ftpState */ } void FtpStateData::listingStart() { debugs(9,3, HERE); wordlist *w; char *dirup; int i, j, k; const char *title = title_url.termedBuf(); flags.listing_started = true; printfReplyBody("\n"); printfReplyBody("\n", version_string); printfReplyBody("\n", mkrfc1123(squid_curtime)); printfReplyBody("\n"); { char *t = xstrdup(title); rfc1738_unescape(t); printfReplyBody("FTP Directory: %s\n", html_quote(t)); xfree(t); } printfReplyBody("\n"); printfReplyBody("\n"); if (flags.need_base_href) printfReplyBody("\n", html_quote(base_href.termedBuf())); printfReplyBody("\n"); if (cwd_message) { printfReplyBody("
\n");

        for (w = cwd_message; w; w = w->next)
            printfReplyBody("%s\n", html_quote(w->key));

        printfReplyBody("
\n"); printfReplyBody("
\n"); wordlistDestroy(&cwd_message); } printfReplyBody("

\n"); printfReplyBody("FTP Directory: "); /* "ftp://" == 6 characters */ assert(title_url.size() >= 6); k = 6 + strcspn(&title[6], "/"); for (i = 6, j = 0; title[i]; j = i) { printfReplyBody(" j) { char *url = xstrdup(title); url[i] = '\0'; printfReplyBody("%s", html_quote(url + k)); printfReplyBody("/"); printfReplyBody("\">"); rfc1738_unescape(url + j); printfReplyBody("%s", html_quote(url + j)); safe_free(url); printfReplyBody(""); } printfReplyBody("/"); if (title[i] == '/') i++; if (i == j) { /* Error guard, or "assert" */ printfReplyBody("ERROR: Failed to parse URL: %s\n", html_quote(title)); debugs(9, DBG_CRITICAL, "Failed to parse URL: " << title); break; } } printfReplyBody("

\n"); printfReplyBody("
\n");
    dirup = htmlifyListEntry("");
    writeReplyBody(dirup, strlen(dirup));
    flags.html_header_sent = 1;
}

void
FtpStateData::listingFinish()
{
    debugs(9,3,HERE);
    entry->buffer();
    printfReplyBody("
\n"); if (flags.listformat_unknown && !flags.tried_nlst) { printfReplyBody("[As plain directory]\n", flags.dir_slash ? rfc1738_escape_part(old_filepath) : "."); } else if (typecode == 'D') { const char *path = flags.dir_slash ? filepath : "."; printfReplyBody("[As extended directory]\n", rfc1738_escape_part(path)); } printfReplyBody("
\n"); printfReplyBody("
\n"); printfReplyBody("Generated %s by %s (%s)\n", mkrfc1123(squid_curtime), getMyHostname(), visible_appname_string); printfReplyBody("
\n"); } /// \ingroup ServerProtocolFTPInternal static const char *Month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; /// \ingroup ServerProtocolFTPInternal static int is_month(const char *buf) { int i; for (i = 0; i < 12; i++) if (!strcasecmp(buf, Month[i])) return 1; return 0; } /// \ingroup ServerProtocolFTPInternal static void ftpListPartsFree(ftpListParts ** parts) { safe_free((*parts)->date); safe_free((*parts)->name); safe_free((*parts)->showname); safe_free((*parts)->link); safe_free(*parts); } /// \ingroup ServerProtocolFTPInternal #define MAX_TOKENS 64 /// \ingroup ServerProtocolFTPInternal static ftpListParts * ftpListParseParts(const char *buf, struct _ftp_flags flags) { ftpListParts *p = NULL; char *t = NULL; const char *ct = NULL; char *tokens[MAX_TOKENS]; int i; int n_tokens; static char tbuf[128]; char *xbuf = NULL; static int scan_ftp_initialized = 0; static regex_t scan_ftp_integer; static regex_t scan_ftp_time; static regex_t scan_ftp_dostime; static regex_t scan_ftp_dosdate; if (!scan_ftp_initialized) { scan_ftp_initialized = 1; regcomp(&scan_ftp_integer, "^[0123456789]+$", REG_EXTENDED | REG_NOSUB); regcomp(&scan_ftp_time, "^[0123456789:]+$", REG_EXTENDED | REG_NOSUB); regcomp(&scan_ftp_dosdate, "^[0123456789]+-[0123456789]+-[0123456789]+$", REG_EXTENDED | REG_NOSUB); regcomp(&scan_ftp_dostime, "^[0123456789]+:[0123456789]+[AP]M$", REG_EXTENDED | REG_NOSUB | REG_ICASE); } if (buf == NULL) return NULL; if (*buf == '\0') return NULL; p = (ftpListParts *)xcalloc(1, sizeof(ftpListParts)); n_tokens = 0; memset(tokens, 0, sizeof(tokens)); xbuf = xstrdup(buf); if (flags.tried_nlst) { /* Machine readable format, one name per line */ p->name = xbuf; p->type = '\0'; return p; } for (t = strtok(xbuf, w_space); t && n_tokens < MAX_TOKENS; t = strtok(NULL, w_space)) tokens[n_tokens++] = xstrdup(t); xfree(xbuf); /* 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]; if (!is_month(month)) continue; if (regexec(&scan_ftp_integer, size, 0, NULL, 0) != 0) continue; if (regexec(&scan_ftp_integer, day, 0, NULL, 0) != 0) continue; if (regexec(&scan_ftp_time, year, 0, NULL, 0) != 0) /* Yr | hh:mm */ continue; snprintf(tbuf, 128, "%s %2s %5s", month, day, year); if (!strstr(buf, tbuf)) snprintf(tbuf, 128, "%s %2s %-5s", month, day, year); char const *copyFrom = NULL; if ((copyFrom = strstr(buf, tbuf))) { p->type = *tokens[0]; p->size = strtoll(size, NULL, 10); p->date = xstrdup(tbuf); if (flags.skip_whitespace) { copyFrom += strlen(tbuf); while (strchr(w_space, *copyFrom)) copyFrom++; } else { /* XXX assumes a single space between date and filename * suggested by: Nathan.Bailey@cc.monash.edu.au and * Mike Battersby */ copyFrom += strlen(tbuf) + 1; } p->name = xstrdup(copyFrom); if (p->type == 'l' && (t = strstr(p->name, " -> "))) { *t = '\0'; p->link = xstrdup(t + 4); } goto found; } break; } /* 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], "")) { p->type = 'd'; } else { p->type = '-'; p->size = strtoll(tokens[2], NULL, 10); } snprintf(tbuf, 128, "%s %s", tokens[0], tokens[1]); p->date = xstrdup(tbuf); if (p->type == 'd') { /* Directory.. name begins with first printable after */ ct = strstr(buf, tokens[2]); ct += strlen(tokens[2]); while (xisspace(*ct)) ct++; if (!*ct) ct = NULL; } 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; } } p->name = xstrdup(ct ? ct : tokens[3]); goto found; } /* Try EPLF format; carson@lehman.com */ if (buf[0] == '+') { ct = buf + 1; p->type = 0; while (ct && *ct) { time_t tm; int l = strcspn(ct, ","); char *tmp; if (l < 1) goto blank; switch (*ct) { case '\t': p->name = xstrndup(ct + 1, l + 1); break; case 's': p->size = atoi(ct + 1); break; case 'm': tm = (time_t) strtol(ct + 1, &tmp, 0); if (tmp != ct + 1) break; /* not a valid integer */ p->date = xstrdup(ctime(&tm)); *(strstr(p->date, "\n")) = '\0'; break; case '/': p->type = 'd'; break; case 'r': p->type = '-'; break; case 'i': break; default: break; } blank: ct = strstr(ct, ","); if (ct) { ct++; } } if (p->type == 0) { p->type = '-'; } if (p->name) goto found; else safe_free(p->date); } found: for (i = 0; i < n_tokens; i++) xfree(tokens[i]); if (!p->name) ftpListPartsFree(&p); /* cleanup */ return p; } /// \ingroup ServerProtocolFTPInternal static const char * dots_fill(size_t len) { static char buf[256]; size_t i = 0; if (len > Config.Ftp.list_width) { memset(buf, ' ', 256); buf[0] = '\n'; buf[Config.Ftp.list_width + 4] = '\0'; return buf; } for (i = len; i < Config.Ftp.list_width; i++) buf[i - len] = (i % 2) ? '.' : ' '; buf[i - len] = '\0'; return buf; } char * FtpStateData::htmlifyListEntry(const char *line) { LOCAL_ARRAY(char, icon, 2048); LOCAL_ARRAY(char, href, 2048 + 40); LOCAL_ARRAY(char, text, 2048); LOCAL_ARRAY(char, size, 2048); LOCAL_ARRAY(char, chdir, 2048 + 40); LOCAL_ARRAY(char, view, 2048 + 40); LOCAL_ARRAY(char, download, 2048 + 40); LOCAL_ARRAY(char, link, 2048 + 40); LOCAL_ARRAY(char, html, 8192); LOCAL_ARRAY(char, prefix, 2048); size_t width = Config.Ftp.list_width; ftpListParts *parts; *icon = *href = *text = *size = *chdir = *view = *download = *link = *html = '\0'; if ((int) strlen(line) > 1024) { snprintf(html, 8192, "%s\n", line); return html; } if (flags.dir_slash && dirpath && typecode != 'D') snprintf(prefix, 2048, "%s/", rfc1738_escape_part(dirpath)); else prefix[0] = '\0'; /* Handle builtin */ if (strcmp(line, "") == 0) { /* {icon} {text} {link} */ snprintf(icon, 2048, "\"%-6s\"", mimeGetIconURL("internal-dirup"), "[DIRUP]"); if (!flags.no_dotdot && !flags.root_dir) { /* Normal directory */ if (!flags.dir_slash) strcpy(href, "../"); else strcpy(href, "./"); strcpy(text, "Parent Directory"); } else if (!flags.no_dotdot && flags.root_dir) { /* "Top level" directory */ strcpy(href, "%2e%2e/"); strcpy(text, "Parent Directory"); snprintf(link, 2048, "(%s)", "%2f/", "Root Directory"); } else if (flags.no_dotdot && !flags.root_dir) { char *url; /* Normal directory where last component is / or .. */ strcpy(href, "%2e%2e/"); strcpy(text, "Parent Directory"); if (flags.dir_slash) { url = xstrdup("./"); } else { const char *title = title_url.termedBuf(); int k = 6 + strcspn(&title[6], "/"); char *t; url = xstrdup(title + k); t = url + strlen(url) - 2; while (t > url && *t != '/') *t-- = '\0'; } snprintf(link, 2048, "(%s)", url, "Back"); safe_free(url); } else { /* NO_DOTDOT && ROOT_DIR */ /* "UNIX Root" directory */ strcpy(href, "/"); strcpy(text, "Home Directory"); } snprintf(html, 8192, "%s %s %s\n", href, icon, href, text, link); return html; } if ((parts = ftpListParseParts(line, flags)) == NULL) { const char *p; snprintf(html, 8192, "%s\n", line); for (p = line; *p && xisspace(*p); p++); if (*p && !xisspace(*p)) flags.listformat_unknown = 1; return html; } if (!strcmp(parts->name, ".") || !strcmp(parts->name, "..")) { *html = '\0'; ftpListPartsFree(&parts); return html; } parts->size += 1023; parts->size >>= 10; parts->showname = xstrdup(parts->name); if (!Config.Ftp.list_wrap) { if (strlen(parts->showname) > width - 1) { *(parts->showname + width - 1) = '>'; *(parts->showname + width - 0) = '\0'; } } /* {icon} {text} . . . {date}{size}{chdir}{view}{download}{link}\n */ xstrncpy(href, rfc1738_escape_part(parts->name), 2048); xstrncpy(text, parts->showname, 2048); switch (parts->type) { case 'd': snprintf(icon, 2048, "\"%-6s\"", mimeGetIconURL("internal-dir"), "[DIR]"); strcat(href, "/"); /* margin is allocated above */ break; case 'l': snprintf(icon, 2048, "\"%-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, " -> %s", *link2 != '/' ? prefix : "", link2, html_quote(parts->link)); safe_free(link2); } break; case '\0': snprintf(icon, 2048, "\"%-6s\"", mimeGetIconURL(parts->name), "[UNKNOWN]"); snprintf(chdir, 2048, " ", rfc1738_escape_part(parts->name), mimeGetIconURL("internal-dir")); break; case '-': default: snprintf(icon, 2048, "\"%-6s\"", mimeGetIconURL(parts->name), "[FILE]"); snprintf(size, 2048, " %6" PRId64 "k", parts->size); break; } if (parts->type != 'd') { if (mimeGetViewOption(parts->name)) { snprintf(view, 2048, " ", prefix, href, mimeGetIconURL("internal-view")); } if (mimeGetDownloadOption(parts->name)) { snprintf(download, 2048, " ", prefix, href, mimeGetIconURL("internal-download")); } } /* {icon} {text} . . . {date}{size}{chdir}{view}{download}{link}\n */ if (parts->type != '\0') { snprintf(html, 8192, "%s %s%s " "%s%8s%s%s%s%s\n", prefix, href, icon, prefix, href, html_quote(text), dots_fill(strlen(text)), parts->date, size, chdir, view, download, link); } else { /* Plain listing. {icon} {text} ... {chdir}{view}{download} */ snprintf(html, 8192, "%s %s%s " "%s%s%s%s\n", prefix, href, icon, prefix, href, html_quote(text), dots_fill(strlen(text)), chdir, view, download, link); } ftpListPartsFree(&parts); return html; } void FtpStateData::parseListing() { char *buf = data.readBuf->content(); char *sbuf; /* NULL-terminated copy of termedBuf */ char *end; char *line; char *s; char *t; size_t linelen; size_t usable; StoreEntry *e = entry; size_t len = data.readBuf->contentSize(); if (!len) { debugs(9, 3, HERE << "no content to parse for " << e->url() ); return; } /* * We need a NULL-terminated buffer for scanning, ick */ sbuf = (char *)xmalloc(len + 1); xstrncpy(sbuf, buf, len + 1); end = sbuf + len - 1; while (*end != '\r' && *end != '\n' && end > sbuf) end--; usable = end - sbuf; debugs(9, 3, HERE << "usable = " << usable); if (usable == 0) { debugs(9, 3, HERE << "didn't find end for " << e->url() ); xfree(sbuf); return; } debugs(9, 3, HERE << (unsigned long int)len << " bytes to play with"); line = (char *)memAllocate(MEM_4K_BUF); end++; e->buffer(); /* released when done processing current data payload */ s = sbuf; s += strspn(s, crlf); for (; s < end; s += strcspn(s, crlf), s += strspn(s, crlf)) { debugs(9, 7, HERE << "s = {" << s << "}"); linelen = strcspn(s, crlf) + 1; if (linelen < 2) break; if (linelen > 4096) linelen = 4096; xstrncpy(line, s, linelen); debugs(9, 7, HERE << "{" << line << "}"); if (!strncmp(line, "total", 5)) continue; t = htmlifyListEntry(line); assert(t != NULL); writeReplyBody(t, strlen(t)); } data.readBuf->consume(usable); memFree(line, MEM_4K_BUF); xfree(sbuf); } int FtpStateData::dataDescriptor() const { return data.fd; } void FtpStateData::dataComplete() { debugs(9, 3,HERE); /* Connection closed; transfer done. */ /// Close data channel, if any, to conserve resources while we wait. data.close(); /* expect the "transfer complete" message on the control socket */ /* * DPW 2007-04-23 * Previously, this was the only place where we set the * 'buffered_ok' flag when calling scheduleReadControlReply(). * It caused some problems if the FTP server returns an unexpected * status code after the data command. FtpStateData was being * deleted in the middle of dataRead(). */ scheduleReadControlReply(0); } void FtpStateData::maybeReadVirginBody() { // too late to read if (data.fd < 0 || fd_table[data.fd].closing()) return; if (data.read_pending) return; const int read_sz = replyBodySpace(*data.readBuf, 0); debugs(11,9, HERE << "FTP may read up to " << read_sz << " bytes"); if (read_sz < 2) // see http.cc return; data.read_pending = true; typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); commSetTimeout(data.fd, Config.Timeout.read, timeoutCall); debugs(9,5,HERE << "queueing read on FD " << data.fd); typedef CommCbMemFunT Dialer; entry->delayAwareRead(data.fd, data.readBuf->space(), read_sz, JobCallback(9, 5, Dialer, this, FtpStateData::dataRead)); } void FtpStateData::dataRead(const CommIoCbParams &io) { int j; int bin; data.read_pending = false; debugs(9, 3, HERE << "ftpDataRead: FD " << io.fd << " Read " << io.size << " bytes"); if (io.size > 0) { kb_incr(&statCounter.server.all.kbytes_in, io.size); kb_incr(&statCounter.server.ftp.kbytes_in, io.size); } if (io.flag == COMM_ERR_CLOSING) return; assert(io.fd == data.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 DELAY_POOLS DelayId delayId = entry->mem_obj->mostBytesAllowed(); delayId.bytesIn(io.size); #endif IOStats.Ftp.reads++; for (j = io.size - 1, bin = 0; j; bin++) j >>= 1; IOStats.Ftp.read_hist[bin]++; } if (io.flag != COMM_OK) { debugs(50, ignoreErrno(io.xerrno) ? 3 : DBG_IMPORTANT, "ftpDataRead: read error: " << xstrerr(io.xerrno)); if (ignoreErrno(io.xerrno)) { typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); commSetTimeout(io.fd, Config.Timeout.read, timeoutCall); maybeReadVirginBody(); } else { if (!flags.http_header_sent && !fwd->ftpPasvFailed() && flags.pasv_supported) { fwd->dontRetry(false); /* this is a retryable error */ fwd->ftpPasvFailed(true); } failed(ERR_READ_ERROR, 0); /* failed closes ctrl.fd and frees ftpState */ return; } } else if (io.size == 0) { debugs(9,3, HERE << "Calling dataComplete() because io.size == 0"); /* * DPW 2007-04-23 * Dangerous curves ahead. This call to dataComplete was * calling scheduleReadControlReply, handleControlReply, * and then ftpReadTransferDone. If ftpReadTransferDone * gets unexpected status code, it closes down the control * socket and our FtpStateData object gets destroyed. As * a workaround we no longer set the 'buffered_ok' flag in * the scheduleReadControlReply call. */ dataComplete(); } processReplyBody(); } void FtpStateData::processReplyBody() { debugs(9, 3, HERE << "FtpStateData::processReplyBody starting."); if (request->method == METHOD_HEAD && (flags.isdir || theSize != -1)) { serverComplete(); return; } if (!flags.http_header_sent && data.readBuf->contentSize() >= 0) appendSuccessHeader(); if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { /* * probably was aborted because content length exceeds one * of the maximum size limits. */ abortTransaction("entry aborted after calling appendSuccessHeader()"); return; } #if USE_ADAPTATION if (adaptationAccessCheckPending) { debugs(9,3, HERE << "returning from FtpStateData::processReplyBody due to adaptationAccessCheckPending"); return; } #endif if (flags.isdir && !flags.listing_started) listingStart(); if (flags.isdir) { parseListing(); } else if (const int csize = data.readBuf->contentSize()) { writeReplyBody(data.readBuf->content(), csize); debugs(9, 5, HERE << "consuming " << csize << " bytes of readBuf"); data.readBuf->consume(csize); } entry->flush(); maybeReadVirginBody(); } /** * Locates the FTP user:password login. * * Highest to lowest priority: * - Checks URL (ftp://user:pass@domain) * - Authorization: Basic header * - squid.conf anonymous-FTP settings (default: anonymous:Squid@). * * Special Case: A username-only may be provided in the URL and password in the HTTP headers. * * 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. */ int FtpStateData::checkAuth(const HttpHeader * req_hdr) { const char *auth; /* default username */ xstrncpy(user, "anonymous", MAX_URL); /* Check HTTP Authorization: headers (better than defaults, but less than URL) */ if ( (auth = req_hdr->getAuth(HDR_AUTHORIZATION, "Basic")) ) { flags.authenticated = 1; loginParser(auth, FTP_LOGIN_NOT_ESCAPED); } /* we fail with authorization-required error later IFF the FTP server requests it */ /* Test URL login syntax. Overrides any headers received. */ loginParser(request->login, FTP_LOGIN_ESCAPED); /* name is missing. thats fatal. */ if (!user[0]) fatal("FTP login parsing destroyed username info"); /* name + password == success */ if (password[0]) return 1; /* Setup default FTP password settings */ /* this has to be done last so that we can have a no-password case above. */ if (!password[0]) { if (strcmp(user, "anonymous") == 0 && !flags.tried_auth_anonymous) { xstrncpy(password, Config.Ftp.anon_user, MAX_URL); flags.tried_auth_anonymous=1; return 1; } else if (!flags.tried_auth_nopass) { xstrncpy(password, null_string, MAX_URL); flags.tried_auth_nopass=1; return 1; } } return 0; /* different username */ } static String str_type_eq; void FtpStateData::checkUrlpath() { int l; size_t t; if (str_type_eq.undefined()) //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); } } l = request->urlpath.size(); /* 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/")) { /* UNIX root directory */ flags.isdir = 1; flags.root_dir = 1; } else if ((l >= 1) && (request->urlpath[l - 1] == '/')) { /* Directory URL, ending in / */ flags.isdir = 1; if (l == 1) flags.root_dir = 1; } else { flags.dir_slash = 1; } } void FtpStateData::buildTitleUrl() { title_url = "ftp://"; if (strcmp(user, "anonymous")) { title_url.append(user); title_url.append("@"); } title_url.append(request->GetHost()); if (request->port != urlDefaultPort(PROTO_FTP)) { title_url.append(":"); title_url.append(xitoa(request->port)); } title_url.append (request->urlpath); base_href = "ftp://"; if (strcmp(user, "anonymous") != 0) { base_href.append(rfc1738_escape_part(user)); if (password_url) { base_href.append (":"); base_href.append(rfc1738_escape_part(password)); } base_href.append("@"); } base_href.append(request->GetHost()); if (request->port != urlDefaultPort(PROTO_FTP)) { base_href.append(":"); base_href.append(xitoa(request->port)); } base_href.append(request->urlpath); base_href.append("/"); } /// \ingroup ServerProtocolFTPAPI void ftpStart(FwdState * fwd) { FtpStateData *ftpState = new FtpStateData(fwd); ftpState->start(); } void FtpStateData::start() { if (!checkAuth(&request->header)) { /* create appropriate reply */ HttpReply *reply = ftpAuthRequired(request, ftpRealm()); entry->replaceHttpReply(reply); serverComplete(); return; } checkUrlpath(); buildTitleUrl(); debugs(9, 5, HERE << "host=" << request->GetHost() << ", path=" << request->urlpath << ", user=" << user << ", passwd=" << password); state = BEGIN; ctrl.last_command = xstrdup("Connect to server"); ctrl.buf = (char *)memAllocBuf(4096, &ctrl.size); ctrl.offset = 0; data.readBuf = new MemBuf; data.readBuf->init(4096, SQUID_TCP_SO_RCVBUF); scheduleReadControlReply(0); } /* ====================================================================== */ /// \ingroup ServerProtocolFTPInternal static char * escapeIAC(const char *buf) { int n; char *ret; unsigned const char *p; unsigned char *r; for (p = (unsigned const char *)buf, n = 1; *p; n++, p++) if (*p == 255) n++; ret = (char *)xmalloc(n); for (p = (unsigned const char *)buf, r=(unsigned char *)ret; *p; p++) { *r++ = *p; if (*p == 255) *r++ = 255; } *r++ = '\0'; assert((r - (unsigned char *)ret) == n ); return ret; } void FtpStateData::writeCommand(const char *buf) { char *ebuf; /* trace FTP protocol communications at level 2 */ debugs(9, 2, "ftp<< " << buf); if (Config.Ftp.telnet) ebuf = escapeIAC(buf); else ebuf = xstrdup(buf); safe_free(ctrl.last_command); safe_free(ctrl.last_reply); ctrl.last_command = ebuf; if (!canSend(ctrl.fd)) { debugs(9, 2, HERE << "cannot send to closing ctrl FD " << ctrl.fd); // TODO: assert(ctrl.closer != NULL); return; } typedef CommCbMemFunT Dialer; AsyncCall::Pointer call = JobCallback(9, 5, Dialer, this, FtpStateData::ftpWriteCommandCallback); comm_write(ctrl.fd, ctrl.last_command, strlen(ctrl.last_command), call); scheduleReadControlReply(0); } void FtpStateData::ftpWriteCommandCallback(const CommIoCbParams &io) { debugs(9, 5, "ftpWriteCommandCallback: wrote " << io.size << " bytes"); if (io.size > 0) { fd_bytes(io.fd, io.size, FD_WRITE); kb_incr(&statCounter.server.all.kbytes_out, io.size); kb_incr(&statCounter.server.ftp.kbytes_out, io.size); } if (io.flag == COMM_ERR_CLOSING) return; if (io.flag) { debugs(9, DBG_IMPORTANT, "ftpWriteCommandCallback: FD " << io.fd << ": " << xstrerr(io.xerrno)); failed(ERR_WRITE_ERROR, io.xerrno); /* failed closes ctrl.fd and frees ftpState */ return; } } wordlist * FtpStateData::ftpParseControlReply(char *buf, size_t len, int *codep, size_t *used) { char *s; char *sbuf; char *end; int usable; int complete = 0; wordlist *head = NULL; wordlist *list; wordlist **tail = &head; size_t offset; size_t linelen; int code = -1; debugs(9, 3, HERE); /* * We need a NULL-terminated buffer for scanning, ick */ sbuf = (char *)xmalloc(len + 1); xstrncpy(sbuf, buf, len + 1); end = sbuf + len - 1; while (*end != '\r' && *end != '\n' && end > sbuf) end--; usable = end - sbuf; debugs(9, 3, HERE << "usable = " << usable); if (usable == 0) { debugs(9, 3, HERE << "didn't find end of line"); safe_free(sbuf); return NULL; } debugs(9, 3, HERE << len << " bytes to play with"); end++; s = sbuf; s += strspn(s, crlf); for (; s < end; s += strcspn(s, crlf), s += strspn(s, crlf)) { if (complete) break; debugs(9, 5, HERE << "s = {" << s << "}"); linelen = strcspn(s, crlf) + 1; if (linelen < 2) break; if (linelen > 3) complete = (*s >= '0' && *s <= '9' && *(s + 3) == ' '); if (complete) code = atoi(s); offset = 0; if (linelen > 3) if (*s >= '0' && *s <= '9' && (*(s + 3) == '-' || *(s + 3) == ' ')) offset = 4; list = new wordlist(); list->key = (char *)xmalloc(linelen - offset); xstrncpy(list->key, s + offset, linelen - offset); /* trace the FTP communication chat at level 2 */ debugs(9, 2, "ftp>> " << code << " " << list->key); *tail = list; tail = &list->next; } *used = (size_t) (s - sbuf); safe_free(sbuf); if (!complete) wordlistDestroy(&head); if (codep) *codep = code; return head; } /** * DPW 2007-04-23 * Looks like there are no longer anymore callers that set * buffered_ok=1. Perhaps it can be removed at some point. */ void FtpStateData::scheduleReadControlReply(int buffered_ok) { debugs(9, 3, HERE << "FD " << ctrl.fd); if (buffered_ok && ctrl.offset > 0) { /* We've already read some reply data */ handleControlReply(); } else { /* XXX What about Config.Timeout.read? */ typedef CommCbMemFunT Dialer; AsyncCall::Pointer reader = JobCallback(9, 5, Dialer, this, FtpStateData::ftpReadControlReply); comm_read(ctrl.fd, ctrl.buf + ctrl.offset, ctrl.size - ctrl.offset, reader); /* * Cancel the timeout on the Data socket (if any) and * establish one on the control socket. */ if (data.fd > -1) { AsyncCall::Pointer nullCall = NULL; commSetTimeout(data.fd, -1, nullCall); } typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); commSetTimeout(ctrl.fd, Config.Timeout.read, timeoutCall); } } void FtpStateData::ftpReadControlReply(const CommIoCbParams &io) { debugs(9, 3, "ftpReadControlReply: FD " << io.fd << ", Read " << io.size << " bytes"); if (io.size > 0) { kb_incr(&statCounter.server.all.kbytes_in, io.size); kb_incr(&statCounter.server.ftp.kbytes_in, io.size); } if (io.flag == COMM_ERR_CLOSING) return; if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { abortTransaction("entry aborted during control reply read"); return; } assert(ctrl.offset < ctrl.size); if (io.flag == COMM_OK && io.size > 0) { fd_bytes(io.fd, io.size, FD_READ); } if (io.flag != COMM_OK) { debugs(50, ignoreErrno(io.xerrno) ? 3 : DBG_IMPORTANT, "ftpReadControlReply: read error: " << xstrerr(io.xerrno)); if (ignoreErrno(io.xerrno)) { scheduleReadControlReply(0); } else { failed(ERR_READ_ERROR, io.xerrno); /* failed closes ctrl.fd and frees ftpState */ } return; } if (io.size == 0) { if (entry->store_status == STORE_PENDING) { failed(ERR_FTP_FAILURE, 0); /* failed closes ctrl.fd and frees ftpState */ return; } /* XXX this may end up having to be serverComplete() .. */ abortTransaction("zero control reply read"); return; } unsigned int len =io.size + ctrl.offset; ctrl.offset = len; assert(len <= ctrl.size); handleControlReply(); } void FtpStateData::handleControlReply() { wordlist **W; size_t bytes_used = 0; wordlistDestroy(&ctrl.message); ctrl.message = ftpParseControlReply(ctrl.buf, ctrl.offset, &ctrl.replycode, &bytes_used); if (ctrl.message == NULL) { /* didn't get complete reply yet */ if (ctrl.offset == ctrl.size) { ctrl.buf = (char *)memReallocBuf(ctrl.buf, ctrl.size << 1, &ctrl.size); } scheduleReadControlReply(0); return; } else if (ctrl.offset == bytes_used) { /* used it all up */ ctrl.offset = 0; } else { /* Got some data past the complete reply */ assert(bytes_used < ctrl.offset); ctrl.offset -= bytes_used; xmemmove(ctrl.buf, ctrl.buf + bytes_used, ctrl.offset); } /* Move the last line of the reply message to ctrl.last_reply */ for (W = &ctrl.message; (*W)->next; W = &(*W)->next); safe_free(ctrl.last_reply); ctrl.last_reply = xstrdup((*W)->key); wordlistDestroy(W); /* Copy the rest of the message to cwd_message to be printed in * error messages */ wordlistAddWl(&cwd_message, ctrl.message); debugs(9, 3, HERE << "state=" << state << ", code=" << ctrl.replycode); FTP_SM_FUNCS[state] (this); } /* ====================================================================== */ /// \ingroup ServerProtocolFTPInternal static void ftpReadWelcome(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (ftpState->flags.pasv_only) ftpState->login_att++; /* Dont retry if the FTP server accepted the connection */ ftpState->fwd->dontRetry(true); if (code == 220) { if (ftpState->ctrl.message) { if (strstr(ftpState->ctrl.message->key, "NetWare")) ftpState->flags.skip_whitespace = 1; } ftpSendUser(ftpState); } else if (code == 120) { if (NULL != ftpState->ctrl.message) debugs(9, DBG_IMPORTANT, "FTP server is busy: " << ftpState->ctrl.message->key); return; } else { ftpFail(ftpState); } } /** * Translate FTP login failure into HTTP error * this is an attmpt to get the 407 message to show up outside Squid. * its NOT a general failure. But a correct FTP response type. */ void FtpStateData::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 = errorCon(ERR_FTP_UNAVAILABLE, HTTP_SERVICE_UNAVAILABLE, fwd->request); } else if (ctrl.replycode >= 430 && ctrl.replycode <= 439) { // 43x - Invalid or Credential Error - retry challenge required. err = errorCon(ERR_FTP_FORBIDDEN, HTTP_UNAUTHORIZED, fwd->request); } 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 = errorCon(ERR_FTP_FORBIDDEN, HTTP_FORBIDDEN, fwd->request); else err = errorCon(ERR_FTP_FORBIDDEN, HTTP_UNAUTHORIZED, fwd->request); } } // 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 "; 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); HttpReply *newrep = err->BuildHttpReply(); errorStateFree(err); /* add Authenticate header */ newrep->header.putAuth("Basic", ftpRealm()); // add it to the store entry for response.... entry->replaceHttpReply(newrep); serverComplete(); } const char * FtpStateData::ftpRealm() { static char realm[8192]; /* 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); } return realm; } /// \ingroup ServerProtocolFTPInternal static void ftpSendUser(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendUser")) return; if (ftpState->proxy_host != NULL) snprintf(cbuf, 1024, "USER %s@%s\r\n", ftpState->user, ftpState->request->GetHost()); else snprintf(cbuf, 1024, "USER %s\r\n", ftpState->user); ftpState->writeCommand(cbuf); ftpState->state = SENT_USER; } /// \ingroup ServerProtocolFTPInternal static void ftpReadUser(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code == 230) { ftpReadPass(ftpState); } else if (code == 331) { ftpSendPass(ftpState); } else { ftpState->loginFailed(); } } /// \ingroup ServerProtocolFTPInternal static void ftpSendPass(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendPass")) return; snprintf(cbuf, 1024, "PASS %s\r\n", ftpState->password); ftpState->writeCommand(cbuf); ftpState->state = SENT_PASS; } /// \ingroup ServerProtocolFTPInternal static void ftpReadPass(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE << "code=" << code); if (code == 230) { ftpSendType(ftpState); } else { ftpState->loginFailed(); } } /// \ingroup ServerProtocolFTPInternal static void ftpSendType(FtpStateData * ftpState) { const char *t; const char *filename; char mode; /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendType")) return; /* * Ref section 3.2.2 of RFC 1738 */ mode = ftpState->typecode; switch (mode) { case 'D': mode = 'A'; break; case 'A': case 'I': break; default: if (ftpState->flags.isdir) { mode = 'A'; } else { t = ftpState->request->urlpath.rpos('/'); filename = t ? t + 1 : ftpState->request->urlpath.termedBuf(); mode = mimeGetTransferMode(filename); } break; } if (mode == 'I') ftpState->flags.binary = 1; else ftpState->flags.binary = 0; snprintf(cbuf, 1024, "TYPE %c\r\n", mode); ftpState->writeCommand(cbuf); ftpState->state = SENT_TYPE; } /// \ingroup ServerProtocolFTPInternal static void ftpReadType(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; char *path; char *d, *p; debugs(9, 3, HERE); if (code == 200) { p = path = xstrdup(ftpState->request->urlpath.termedBuf()); if (*p == '/') p++; while (*p) { d = p; p += strcspn(p, "/"); if (*p) *p++ = '\0'; rfc1738_unescape(d); if (*d) wordlistAdd(&ftpState->pathcomps, d); } xfree(path); if (ftpState->pathcomps) ftpTraverseDirectory(ftpState); else ftpListDir(ftpState); } else { ftpFail(ftpState); } } /// \ingroup ServerProtocolFTPInternal static void ftpTraverseDirectory(FtpStateData * ftpState) { wordlist *w; debugs(9, 4, HERE << (ftpState->filepath ? ftpState->filepath : "")); safe_free(ftpState->dirpath); ftpState->dirpath = ftpState->filepath; ftpState->filepath = NULL; /* Done? */ if (ftpState->pathcomps == NULL) { debugs(9, 3, HERE << "the final component was a directory"); ftpListDir(ftpState); return; } /* Go to next path component */ w = ftpState->pathcomps; ftpState->filepath = w->key; ftpState->pathcomps = w->next; delete w; /* Check if we are to CWD or RETR */ if (ftpState->pathcomps != NULL || ftpState->flags.isdir) { ftpSendCwd(ftpState); } else { debugs(9, 3, HERE << "final component is probably a file"); ftpGetFile(ftpState); return; } } /// \ingroup ServerProtocolFTPInternal static void ftpSendCwd(FtpStateData * ftpState) { char *path = NULL; /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendCwd")) return; debugs(9, 3, HERE); path = ftpState->filepath; if (!strcmp(path, "..") || !strcmp(path, "/")) { ftpState->flags.no_dotdot = 1; } else { ftpState->flags.no_dotdot = 0; } snprintf(cbuf, 1024, "CWD %s\r\n", path); ftpState->writeCommand(cbuf); ftpState->state = SENT_CWD; } /// \ingroup ServerProtocolFTPInternal static void ftpReadCwd(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code >= 200 && code < 300) { /* CWD OK */ ftpState->unhack(); /* Reset cwd_message to only include the last message */ if (ftpState->cwd_message) wordlistDestroy(&ftpState->cwd_message); ftpState->cwd_message = ftpState->ctrl.message; ftpState->ctrl.message = NULL; /* Continue to traverse the path */ ftpTraverseDirectory(ftpState); } else { /* CWD FAILED */ if (!ftpState->flags.put) ftpFail(ftpState); else ftpSendMkdir(ftpState); } } /// \ingroup ServerProtocolFTPInternal static void ftpSendMkdir(FtpStateData * ftpState) { char *path = NULL; /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendMkdir")) return; path = ftpState->filepath; debugs(9, 3, HERE << "with path=" << path); snprintf(cbuf, 1024, "MKD %s\r\n", path); ftpState->writeCommand(cbuf); ftpState->state = SENT_MKDIR; } /// \ingroup ServerProtocolFTPInternal static void ftpReadMkdir(FtpStateData * ftpState) { char *path = ftpState->filepath; int code = ftpState->ctrl.replycode; debugs(9, 3, HERE << "path " << path << ", code " << code); if (code == 257) { /* success */ ftpSendCwd(ftpState); } else if (code == 550) { /* dir exists */ if (ftpState->flags.put_mkdir) { ftpState->flags.put_mkdir = 1; ftpSendCwd(ftpState); } else ftpSendReply(ftpState); } else ftpSendReply(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpGetFile(FtpStateData * ftpState) { assert(*ftpState->filepath != '\0'); ftpState->flags.isdir = 0; ftpSendMdtm(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpListDir(FtpStateData * ftpState) { if (ftpState->flags.dir_slash) { debugs(9, 3, HERE << "Directory path did not end in /"); ftpState->title_url.append("/"); ftpState->flags.isdir = 1; } ftpSendPassive(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpSendMdtm(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendMdtm")) return; assert(*ftpState->filepath != '\0'); snprintf(cbuf, 1024, "MDTM %s\r\n", ftpState->filepath); ftpState->writeCommand(cbuf); ftpState->state = SENT_MDTM; } /// \ingroup ServerProtocolFTPInternal static void ftpReadMdtm(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code == 213) { ftpState->mdtm = parse_iso3307_time(ftpState->ctrl.last_reply); ftpState->unhack(); } else if (code < 0) { ftpFail(ftpState); return; } ftpSendSize(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpSendSize(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendSize")) return; /* Only send SIZE for binary transfers. The returned size * is useless on ASCII transfers */ if (ftpState->flags.binary) { assert(ftpState->filepath != NULL); assert(*ftpState->filepath != '\0'); snprintf(cbuf, 1024, "SIZE %s\r\n", ftpState->filepath); ftpState->writeCommand(cbuf); ftpState->state = SENT_SIZE; } else /* Skip to next state no non-binary transfers */ ftpSendPassive(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpReadSize(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code == 213) { ftpState->unhack(); ftpState->theSize = strtoll(ftpState->ctrl.last_reply, NULL, 10); if (ftpState->theSize == 0) { debugs(9, 2, "SIZE reported " << ftpState->ctrl.last_reply << " on " << ftpState->title_url); ftpState->theSize = -1; } } else if (code < 0) { ftpFail(ftpState); return; } ftpSendPassive(ftpState); } /** \ingroup ServerProtocolFTPInternal */ static void ftpReadEPSV(FtpStateData* ftpState) { int code = ftpState->ctrl.replycode; char h1, h2, h3, h4; unsigned short port; IpAddress ipa_remote; int fd = ftpState->data.fd; 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 " << fd_table[ftpState->ctrl.fd].ipaddr << ". Wrong accept code for EPSV"); } else { debugs(9, 2, "EPSV not supported by remote end"); ftpState->state = SENT_EPSV_1; /* simulate having failed EPSV 1 (last EPSV to try before shifting to PASV) */ } ftpSendPassive(ftpState); return; } 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: " << ftpState->ctrl.last_reply); buf = ftpState->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 " << fd_table[ftpState->ctrl.fd].ipaddr << ". 522 error missing protocol negotiation hints"); ftpSendPassive(ftpState); } else if (strcmp(buf, "(1)") == 0) { ftpState->state = SENT_EPSV_2; /* simulate having sent and failed EPSV 2 */ ftpSendPassive(ftpState); } 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 (ftpState->state == SENT_EPSV_2) { ftpSendEPRT(ftpState); } else { /* or try the next Passive mode down the chain. */ ftpSendPassive(ftpState); } } else { /* Server only accept EPSV in IPv6 traffic. */ ftpState->state = SENT_EPSV_1; /* simulate having sent and failed EPSV 1 */ ftpSendPassive(ftpState); } } else { /* handle broken server (RFC 2428 says MUST specify supported protocols in 522) */ debugs(9, DBG_IMPORTANT, "WARNING: Server at " << fd_table[ftpState->ctrl.fd].ipaddr << " sent unknown protocol negotiation hint: " << buf); ftpSendPassive(ftpState); } return; } /* 229 Entering Extended Passive Mode (|||port|) */ /* ANSI sez [^0-9] is undefined, it breaks on Watcom cc */ debugs(9, 5, "scanning: " << ftpState->ctrl.last_reply); buf = ftpState->ctrl.last_reply + strcspn(ftpState->ctrl.last_reply, "("); sscanf(buf, "(%c%c%c%hu%c)", &h1, &h2, &h3, &port, &h4); if (h1 != h2 || h1 != h3 || h1 != h4) { debugs(9, DBG_IMPORTANT, "Invalid EPSV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendPassive(ftpState); return; } if (0 == port) { debugs(9, DBG_IMPORTANT, "Unsafe EPSV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendPassive(ftpState); return; } if (Config.Ftp.sanitycheck) { if (port < 1024) { debugs(9, DBG_IMPORTANT, "Unsafe EPSV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendPassive(ftpState); return; } } ftpState->data.port = port; safe_free(ftpState->data.host); ftpState->data.host = xstrdup(fd_table[ftpState->ctrl.fd].ipaddr); safe_free(ftpState->ctrl.last_command); safe_free(ftpState->ctrl.last_reply); ftpState->ctrl.last_command = xstrdup("Connect to server data port"); debugs(9, 3, HERE << "connecting to " << ftpState->data.host << ", port " << ftpState->data.port); commConnectStart(fd, ftpState->data.host, port, FtpStateData::ftpPasvCallback, ftpState); } /** \ingroup ServerProtocolFTPInternal * * Send Passive connection request. * Default method is to use modern EPSV request. * The failover mechanism should check for previous state and re-call with alternates on failure. */ static void ftpSendPassive(FtpStateData * ftpState) { IpAddress addr; struct addrinfo *AI = NULL; /** Checks the server control channel is still available before running. */ if (!ftpState || !ftpState->haveControlChannel("ftpSendPassive")) return; 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 && ftpState->flags.epsv_all_sent && ftpState->state == SENT_EPSV_1 ) { debugs(9, DBG_IMPORTANT, "FTP does not allow PASV method after 'EPSV ALL' has been sent."); ftpFail(ftpState); return; } /** \par * Checks for 'HEAD' method request and passes off for special handling by FtpStateData::processHeadResponse(). */ if (ftpState->request->method == METHOD_HEAD && (ftpState->flags.isdir || ftpState->theSize != -1)) { ftpState->processHeadResponse(); // may call serverComplete return; } /// 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 * Locates the Address of the remote server. */ addr.InitAddrInfo(AI); if (getsockname(ftpState->ctrl.fd, AI->ai_addr, &AI->ai_addrlen)) { /** If it cannot be located the FTP Session is killed. */ addr.FreeAddrInfo(AI); debugs(9, DBG_CRITICAL, HERE << "getsockname(" << ftpState->ctrl.fd << ",'" << addr << "',...): " << xstrerror()); ftpFail(ftpState); return; } addr = *AI; addr.FreeAddrInfo(AI); /** \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 (addr.IsIPv6()) { debugs(9, 5, HERE << "FTP Channel is IPv6 (" << addr << ") attempting EPSV 2 after EPSV ALL has failed."); snprintf(cbuf, 1024, "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 (addr.IsIPv4()) { debugs(9, 5, HERE << "FTP Channel is IPv4 (" << addr << ") attempting EPSV 1 after EPSV ALL has failed."); snprintf(cbuf, 1024, "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 (" << addr << ") rejects EPSV connection attempts. Trying PASV instead."); snprintf(cbuf, 1024, "PASV\r\n"); ftpState->state = SENT_PASV; break; default: if (!Config.Ftp.epsv) { debugs(9, 5, HERE << "EPSV support manually disabled. Sending PASV for FTP Channel (" << addr <<")"); snprintf(cbuf, 1024, "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 (" << addr <<")"); snprintf(cbuf, 1024, "EPSV ALL\r\n"); ftpState->state = SENT_EPSV_ALL; /* block other non-EPSV connections being attempted */ ftpState->flags.epsv_all_sent = true; } else { if (addr.IsIPv6()) { debugs(9, 5, HERE << "FTP Channel (" << addr << "). Sending default EPSV 2"); snprintf(cbuf, 1024, "EPSV 2\r\n"); ftpState->state = SENT_EPSV_2; } if (addr.IsIPv4()) { debugs(9, 5, HERE << "Channel (" << addr <<"). Sending default EPSV 1"); snprintf(cbuf, 1024, "EPSV 1\r\n"); ftpState->state = SENT_EPSV_1; } } break; } /** Otherwise, Open data channel with the same local address as control channel (on a new random port!) */ addr.SetPort(0); int fd = comm_open(SOCK_STREAM, IPPROTO_TCP, addr, COMM_NONBLOCKING, ftpState->entry->url()); debugs(9, 3, HERE << "Unconnected data socket created on FD " << fd << " from " << addr); if (fd < 0) { ftpFail(ftpState); return; } ftpState->data.opened(fd, ftpState->dataCloser()); ftpState->writeCommand(cbuf); /* * ugly hack for ftp servers like ftp.netscape.com that sometimes * dont acknowledge PASV commands. */ typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, ftpState, FtpStateData::ftpTimeout); commSetTimeout(ftpState->data.fd, 15, timeoutCall); } void FtpStateData::processHeadResponse() { debugs(9, 5, HERE << "handling HEAD response"); ftpSendQuit(this); appendSuccessHeader(); /* * On rare occasions I'm seeing the entry get aborted after * ftpReadControlReply() and before here, probably when * trying to write to the client. */ if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { abortTransaction("entry aborted while processing HEAD"); return; } #if USE_ADAPTATION if (adaptationAccessCheckPending) { debugs(9,3, HERE << "returning due to adaptationAccessCheckPending"); return; } #endif // processReplyBody calls serverComplete() since there is no body processReplyBody(); } /// \ingroup ServerProtocolFTPInternal static void ftpReadPasv(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; int h1, h2, h3, h4; int p1, p2; int n; unsigned short port; IpAddress ipa_remote; int fd = ftpState->data.fd; char *buf; LOCAL_ARRAY(char, ipaddr, 1024); debugs(9, 3, HERE); if (code != 227) { debugs(9, 2, "PASV not supported by remote end"); ftpSendEPRT(ftpState); return; } /* 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). */ /* ANSI sez [^0-9] is undefined, it breaks on Watcom cc */ debugs(9, 5, HERE << "scanning: " << ftpState->ctrl.last_reply); buf = ftpState->ctrl.last_reply + strcspn(ftpState->ctrl.last_reply, "0123456789"); n = sscanf(buf, "%d,%d,%d,%d,%d,%d", &h1, &h2, &h3, &h4, &p1, &p2); if (n != 6 || p1 < 0 || p2 < 0 || p1 > 255 || p2 > 255) { debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendEPRT(ftpState); return; } snprintf(ipaddr, 1024, "%d.%d.%d.%d", h1, h2, h3, h4); ipa_remote = ipaddr; if ( ipa_remote.IsAnyAddr() ) { debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendEPRT(ftpState); return; } port = ((p1 << 8) + p2); if (0 == port) { debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendEPRT(ftpState); return; } if (Config.Ftp.sanitycheck) { if (port < 1024) { debugs(9, DBG_IMPORTANT, "Unsafe PASV reply from " << fd_table[ftpState->ctrl.fd].ipaddr << ": " << ftpState->ctrl.last_reply); ftpSendEPRT(ftpState); return; } } ftpState->data.port = port; safe_free(ftpState->data.host); if (Config.Ftp.sanitycheck) ftpState->data.host = xstrdup(fd_table[ftpState->ctrl.fd].ipaddr); else ftpState->data.host = xstrdup(ipaddr); safe_free(ftpState->ctrl.last_command); safe_free(ftpState->ctrl.last_reply); ftpState->ctrl.last_command = xstrdup("Connect to server data port"); debugs(9, 3, HERE << "connecting to " << ftpState->data.host << ", port " << ftpState->data.port); commConnectStart(fd, ipaddr, port, FtpStateData::ftpPasvCallback, ftpState); } void FtpStateData::ftpPasvCallback(int fd, const DnsLookupDetails &dns, comm_err_t status, int xerrno, void *data) { FtpStateData *ftpState = (FtpStateData *)data; debugs(9, 3, HERE); ftpState->request->recordLookup(dns); if (status != COMM_OK) { debugs(9, 2, HERE << "Failed to connect. Retrying without PASV."); ftpState->fwd->dontRetry(false); /* this is a retryable error */ ftpState->fwd->ftpPasvFailed(true); ftpState->failed(ERR_NONE, 0); /* failed closes ctrl.fd and frees ftpState */ return; } ftpRestOrList(ftpState); } /// \ingroup ServerProtocolFTPInternal static int ftpOpenListenSocket(FtpStateData * ftpState, int fallback) { int fd; IpAddress addr; struct addrinfo *AI = NULL; int on = 1; int x = 0; /// Close old data channel, if any. We may open a new one below. ftpState->data.close(); safe_free(ftpState->data.host); /* * Set up a listen socket on the same local address as the * control connection. */ addr.InitAddrInfo(AI); x = getsockname(ftpState->ctrl.fd, AI->ai_addr, &AI->ai_addrlen); addr = *AI; addr.FreeAddrInfo(AI); if (x) { debugs(9, DBG_CRITICAL, HERE << "getsockname(" << ftpState->ctrl.fd << ",..): " << xstrerror()); return -1; } /* * REUSEADDR is needed in fallback mode, since the same port is * used for both control and data. */ if (fallback) { setsockopt(ftpState->ctrl.fd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on)); } else { /* if not running in fallback mode a new port needs to be retrieved */ addr.SetPort(0); } fd = comm_open(SOCK_STREAM, IPPROTO_TCP, addr, COMM_NONBLOCKING | (fallback ? COMM_REUSEADDR : 0), ftpState->entry->url()); debugs(9, 3, HERE << "Unconnected data socket created on FD " << fd ); if (fd < 0) { debugs(9, DBG_CRITICAL, HERE << "comm_open failed"); return -1; } if (comm_listen(fd) < 0) { comm_close(fd); return -1; } ftpState->data.opened(fd, ftpState->dataCloser()); ftpState->data.port = comm_local_port(fd); ftpState->data.host = NULL; return fd; } /// \ingroup ServerProtocolFTPInternal static void ftpSendPORT(FtpStateData * ftpState) { int fd; IpAddress ipa; struct addrinfo *AI = NULL; unsigned char *addrptr; unsigned char *portptr; /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendPort")) return; if (Config.Ftp.epsv_all && ftpState->flags.epsv_all_sent) { debugs(9, DBG_IMPORTANT, "FTP does not allow PORT method after 'EPSV ALL' has been sent."); return; } debugs(9, 3, HERE); ftpState->flags.pasv_supported = 0; fd = ftpOpenListenSocket(ftpState, 0); ipa.InitAddrInfo(AI); if (getsockname(fd, AI->ai_addr, &AI->ai_addrlen)) { ipa.FreeAddrInfo(AI); debugs(9, DBG_CRITICAL, HERE << "getsockname(" << fd << ",..): " << xstrerror()); /* XXX Need to set error message */ ftpFail(ftpState); return; } if ( AI->ai_addrlen != sizeof(struct sockaddr_in) ) { ipa.FreeAddrInfo(AI); /* IPv6 CANNOT send PORT command. */ /* we got here by attempting and failing an EPRT */ /* using the same reply code should simulate a PORT failure */ ftpReadPORT(ftpState); return; } addrptr = (unsigned char *) &((struct sockaddr_in*)AI->ai_addr)->sin_addr; portptr = (unsigned char *) &((struct sockaddr_in*)AI->ai_addr)->sin_port; snprintf(cbuf, 1024, "PORT %d,%d,%d,%d,%d,%d\r\n", addrptr[0], addrptr[1], addrptr[2], addrptr[3], portptr[0], portptr[1]); ftpState->writeCommand(cbuf); ftpState->state = SENT_PORT; ipa.FreeAddrInfo(AI); } /// \ingroup ServerProtocolFTPInternal static void ftpReadPORT(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code != 200) { /* Fall back on using the same port as the control connection */ debugs(9, 3, "PORT not supported by remote end"); ftpOpenListenSocket(ftpState, 1); } ftpRestOrList(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpSendEPRT(FtpStateData * ftpState) { 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; } if (!Config.Ftp.eprt) { /* Disabled. Switch immediately to attempting old PORT command. */ debugs(9, 3, "EPRT disabled by local administrator"); ftpSendPORT(ftpState); return; } int fd; IpAddress addr; struct addrinfo *AI = NULL; char buf[MAX_IPSTRLEN]; debugs(9, 3, HERE); ftpState->flags.pasv_supported = 0; fd = ftpOpenListenSocket(ftpState, 0); addr.InitAddrInfo(AI); if (getsockname(fd, AI->ai_addr, &AI->ai_addrlen)) { addr.FreeAddrInfo(AI); debugs(9, DBG_CRITICAL, HERE << "getsockname(" << fd << ",..): " << xstrerror()); /* XXX Need to set error message */ ftpFail(ftpState); return; } addr = *AI; /* RFC 2428 defines EPRT as IPv6 equivalent to IPv4 PORT command. */ /* Which can be used by EITHER protocol. */ snprintf(cbuf, 1024, "EPRT |%d|%s|%d|\r\n", ( addr.IsIPv6() ? 2 : 1 ), addr.NtoA(buf,MAX_IPSTRLEN), addr.GetPort() ); ftpState->writeCommand(cbuf); ftpState->state = SENT_EPRT; addr.FreeAddrInfo(AI); } static void ftpReadEPRT(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code != 200) { /* Failover to attempting old PORT command. */ debugs(9, 3, "EPRT not supported by remote end"); ftpSendPORT(ftpState); return; } ftpRestOrList(ftpState); } /** \ingroup ServerProtocolFTPInternal \par * "read" handler to accept FTP data connections. * \param io comm accept(2) callback parameters */ void FtpStateData::ftpAcceptDataConnection(const CommAcceptCbParams &io) { char ntoapeer[MAX_IPSTRLEN]; debugs(9, 3, "ftpAcceptDataConnection"); if (io.flag == COMM_ERR_CLOSING) return; if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) { abortTransaction("entry aborted when accepting data conn"); return; } if (io.flag != COMM_OK) { data.close(); debugs(9, DBG_IMPORTANT, "FTP AcceptDataConnection: FD " << io.fd << ": " << xstrerr(io.xerrno)); /** \todo Need to send error message on control channel*/ ftpFail(this); return; } /** \par * When squid.conf ftp_sanitycheck is enabled, check the new connection is actually being * made by the remote client which is connected to the FTP control socket. * This prevents third-party hacks, but also third-party load balancing handshakes. */ if (Config.Ftp.sanitycheck) { io.details.peer.NtoA(ntoapeer,MAX_IPSTRLEN); if (strcmp(fd_table[ctrl.fd].ipaddr, ntoapeer) != 0) { debugs(9, DBG_IMPORTANT, "FTP data connection from unexpected server (" << io.details.peer << "), expecting " << fd_table[ctrl.fd].ipaddr); comm_close(io.nfd); typedef CommCbMemFunT acceptDialer; AsyncCall::Pointer acceptCall = JobCallback(11, 5, acceptDialer, this, FtpStateData::ftpAcceptDataConnection); comm_accept(data.fd, acceptCall); return; } } /**\par * Replace the Listen socket with the accepted data socket */ data.close(); data.opened(io.nfd, dataCloser()); data.port = io.details.peer.GetPort(); io.details.peer.NtoA(ntoapeer,sizeof(ntoapeer)); data.host = xstrdup(ntoapeer); debugs(9, 3, "ftpAcceptDataConnection: Connected data socket on " << "FD " << io.nfd << " to " << io.details.peer << " FD table says: " << "ctrl-peer= " << fd_table[ctrl.fd].ipaddr << ", " << "data-peer= " << fd_table[data.fd].ipaddr); AsyncCall::Pointer nullCall = NULL; commSetTimeout(ctrl.fd, -1, nullCall); typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); commSetTimeout(data.fd, Config.Timeout.read, timeoutCall); /*\todo XXX We should have a flag to track connect state... * host NULL -> not connected, port == local port * host set -> connected, port == remote port */ /* Restart state (SENT_NLST/LIST/RETR) */ FTP_SM_FUNCS[state] (this); } /// \ingroup ServerProtocolFTPInternal static void ftpRestOrList(FtpStateData * ftpState) { debugs(9, 3, HERE); if (ftpState->typecode == 'D') { ftpState->flags.isdir = 1; if (ftpState->flags.put) { ftpSendMkdir(ftpState); /* PUT name;type=d */ } else { ftpSendNlst(ftpState); /* GET name;type=d sec 3.2.2 of RFC 1738 */ } } else if (ftpState->flags.put) { ftpSendStor(ftpState); } else if (ftpState->flags.isdir) ftpSendList(ftpState); else if (ftpState->restartable()) ftpSendRest(ftpState); else ftpSendRetr(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpSendStor(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendStor")) return; debugs(9, 3, HERE); if (ftpState->filepath != NULL) { /* Plain file upload */ snprintf(cbuf, 1024, "STOR %s\r\n", ftpState->filepath); ftpState->writeCommand(cbuf); ftpState->state = SENT_STOR; } else if (ftpState->request->header.getInt64(HDR_CONTENT_LENGTH) > 0) { /* File upload without a filename. use STOU to generate one */ snprintf(cbuf, 1024, "STOU\r\n"); ftpState->writeCommand(cbuf); ftpState->state = SENT_STOR; } else { /* No file to transfer. Only create directories if needed */ ftpSendReply(ftpState); } } /// \ingroup ServerProtocolFTPInternal /// \deprecated use ftpState->readStor() instead. static void ftpReadStor(FtpStateData * ftpState) { ftpState->readStor(); } void FtpStateData::readStor() { int code = ctrl.replycode; debugs(9, 3, HERE); if (code == 125 || (code == 150 && data.host)) { if (!startRequestBodyFlow()) { // register to receive body data ftpFail(this); return; } /*\par * When client status is 125, or 150 without a hostname, Begin data transfer. */ debugs(9, 3, HERE << "starting data transfer"); sendMoreRequestBody(); /** \par * Cancel the timeout on the Control socket and * establish one on the data socket. */ AsyncCall::Pointer nullCall = NULL; commSetTimeout(ctrl.fd, -1, nullCall); typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, this, FtpStateData::ftpTimeout); commSetTimeout(data.fd, Config.Timeout.read, timeoutCall); state = WRITING_DATA; debugs(9, 3, HERE << "writing data channel"); } else if (code == 150) { /*\par * When client code is 150 with a hostname, Accept data channel. */ debugs(9, 3, "ftpReadStor: accepting data channel"); typedef CommCbMemFunT acceptDialer; AsyncCall::Pointer acceptCall = JobCallback(11, 5, acceptDialer, this, FtpStateData::ftpAcceptDataConnection); comm_accept(data.fd, acceptCall); } else { debugs(9, DBG_IMPORTANT, HERE << "Unexpected reply code "<< std::setfill('0') << std::setw(3) << code); ftpFail(this); } } /// \ingroup ServerProtocolFTPInternal static void ftpSendRest(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendRest")) return; debugs(9, 3, HERE); snprintf(cbuf, 1024, "REST %" PRId64 "\r\n", ftpState->restart_offset); ftpState->writeCommand(cbuf); ftpState->state = SENT_REST; } int FtpStateData::restartable() { if (restart_offset > 0) return 1; if (!request->range) return 0; if (!flags.binary) return 0; if (theSize <= 0) return 0; int64_t desired_offset = request->range->lowestOffset(theSize); if (desired_offset <= 0) return 0; if (desired_offset >= theSize) return 0; restart_offset = desired_offset; return 1; } /// \ingroup ServerProtocolFTPInternal static void ftpReadRest(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); assert(ftpState->restart_offset > 0); if (code == 350) { ftpState->setCurrentOffset(ftpState->restart_offset); ftpSendRetr(ftpState); } else if (code > 0) { debugs(9, 3, HERE << "REST not supported"); ftpState->flags.rest_supported = 0; ftpSendRetr(ftpState); } else { ftpFail(ftpState); } } /// \ingroup ServerProtocolFTPInternal static void ftpSendList(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendList")) return; debugs(9, 3, HERE); if (ftpState->filepath) { snprintf(cbuf, 1024, "LIST %s\r\n", ftpState->filepath); } else { snprintf(cbuf, 1024, "LIST\r\n"); } ftpState->writeCommand(cbuf); ftpState->state = SENT_LIST; } /// \ingroup ServerProtocolFTPInternal static void ftpSendNlst(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendNlst")) return; debugs(9, 3, HERE); ftpState->flags.tried_nlst = 1; if (ftpState->filepath) { snprintf(cbuf, 1024, "NLST %s\r\n", ftpState->filepath); } else { snprintf(cbuf, 1024, "NLST\r\n"); } ftpState->writeCommand(cbuf); ftpState->state = SENT_NLST; } /// \ingroup ServerProtocolFTPInternal static void ftpReadList(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code == 125 || (code == 150 && ftpState->data.host)) { /* Begin data transfer */ /* XXX what about Config.Timeout.read? */ ftpState->maybeReadVirginBody(); ftpState->state = READING_DATA; /* * Cancel the timeout on the Control socket and establish one * on the data socket */ AsyncCall::Pointer nullCall = NULL; commSetTimeout(ftpState->ctrl.fd, -1, nullCall); return; } else if (code == 150) { /* Accept data channel */ typedef CommCbMemFunT acceptDialer; AsyncCall::Pointer acceptCall = JobCallback(11, 5, acceptDialer, ftpState, FtpStateData::ftpAcceptDataConnection); comm_accept(ftpState->data.fd, acceptCall); /* * Cancel the timeout on the Control socket and establish one * on the data socket */ AsyncCall::Pointer nullCall = NULL; commSetTimeout(ftpState->ctrl.fd, -1, nullCall); typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, ftpState,FtpStateData::ftpTimeout); commSetTimeout(ftpState->data.fd, Config.Timeout.read, timeoutCall); return; } else if (!ftpState->flags.tried_nlst && code > 300) { ftpSendNlst(ftpState); } else { ftpFail(ftpState); return; } } /// \ingroup ServerProtocolFTPInternal static void ftpSendRetr(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendRetr")) return; debugs(9, 3, HERE); assert(ftpState->filepath != NULL); snprintf(cbuf, 1024, "RETR %s\r\n", ftpState->filepath); ftpState->writeCommand(cbuf); ftpState->state = SENT_RETR; } /// \ingroup ServerProtocolFTPInternal static void ftpReadRetr(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code == 125 || (code == 150 && ftpState->data.host)) { /* Begin data transfer */ debugs(9, 3, HERE << "reading data channel"); /* XXX what about Config.Timeout.read? */ ftpState->maybeReadVirginBody(); ftpState->state = READING_DATA; /* * Cancel the timeout on the Control socket and establish one * on the data socket */ AsyncCall::Pointer nullCall = NULL; commSetTimeout(ftpState->ctrl.fd, -1, nullCall); } else if (code == 150) { /* Accept data channel */ typedef CommCbMemFunT acceptDialer; AsyncCall::Pointer acceptCall = JobCallback(11, 5, acceptDialer, ftpState, FtpStateData::ftpAcceptDataConnection); comm_accept(ftpState->data.fd, acceptCall); /* * Cancel the timeout on the Control socket and establish one * on the data socket */ AsyncCall::Pointer nullCall = NULL; commSetTimeout(ftpState->ctrl.fd, -1, nullCall); typedef CommCbMemFunT TimeoutDialer; AsyncCall::Pointer timeoutCall = JobCallback(9, 5, TimeoutDialer, ftpState,FtpStateData::ftpTimeout); commSetTimeout(ftpState->data.fd, Config.Timeout.read, timeoutCall); } else if (code >= 300) { if (!ftpState->flags.try_slash_hack) { /* Try this as a directory missing trailing slash... */ ftpState->hackShortcut(ftpSendCwd); } else { ftpFail(ftpState); } } else { ftpFail(ftpState); } } /// \ingroup ServerProtocolFTPInternal static void ftpReadTransferDone(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (code == 226 || code == 250) { /* Connection closed; retrieval done. */ if (ftpState->flags.html_header_sent) ftpState->listingFinish(); ftpSendQuit(ftpState); } else { /* != 226 */ debugs(9, DBG_IMPORTANT, HERE << "Got code " << code << " after reading data"); ftpState->failed(ERR_FTP_FAILURE, 0); /* failed closes ctrl.fd and frees ftpState */ return; } } // premature end of the request body void FtpStateData::handleRequestBodyProducerAborted() { ServerStateData::handleRequestBodyProducerAborted(); debugs(9, 3, HERE << "ftpState=" << this); failed(ERR_READ_ERROR, 0); } /** * This will be called when the put write is completed */ void FtpStateData::sentRequestBody(const CommIoCbParams &io) { if (io.size > 0) kb_incr(&statCounter.server.ftp.kbytes_out, io.size); ServerStateData::sentRequestBody(io); } /// \ingroup ServerProtocolFTPInternal static void ftpWriteTransferDone(FtpStateData * ftpState) { int code = ftpState->ctrl.replycode; debugs(9, 3, HERE); if (!(code == 226 || code == 250)) { debugs(9, DBG_IMPORTANT, HERE << "Got code " << code << " after sending data"); ftpState->failed(ERR_FTP_PUT_ERROR, 0); return; } ftpState->entry->timestampsSet(); /* XXX Is this needed? */ ftpSendReply(ftpState); } /// \ingroup ServerProtocolFTPInternal static void ftpSendQuit(FtpStateData * ftpState) { /* check the server control channel is still available */ if (!ftpState || !ftpState->haveControlChannel("ftpSendQuit")) return; snprintf(cbuf, 1024, "QUIT\r\n"); ftpState->writeCommand(cbuf); ftpState->state = SENT_QUIT; } /// \ingroup ServerProtocolFTPInternal static void ftpReadQuit(FtpStateData * ftpState) { /** \todo XXX should this just be a case of abortTransaction? */ ftpState->serverComplete(); } /// \ingroup ServerProtocolFTPInternal static void ftpTrySlashHack(FtpStateData * ftpState) { char *path; ftpState->flags.try_slash_hack = 1; /* Free old paths */ debugs(9, 3, HERE); if (ftpState->pathcomps) wordlistDestroy(&ftpState->pathcomps); safe_free(ftpState->filepath); /* Build the new path (urlpath begins with /) */ path = xstrdup(ftpState->request->urlpath.termedBuf()); rfc1738_unescape(path); ftpState->filepath = path; /* And off we go */ ftpGetFile(ftpState); } /** * Forget hack status. Next error is shown to the user */ void FtpStateData::unhack() { debugs(9, 3, HERE); if (old_request != NULL) { safe_free(old_request); safe_free(old_reply); } } void FtpStateData::hackShortcut(FTPSM * nextState) { /* Clear some unwanted state */ setCurrentOffset(0); restart_offset = 0; /* Save old error message & some state info */ debugs(9, 3, HERE); if (old_request == NULL) { old_request = ctrl.last_command; ctrl.last_command = NULL; old_reply = ctrl.last_reply; ctrl.last_reply = NULL; if (pathcomps == NULL && filepath != NULL) old_filepath = xstrdup(filepath); } /* Jump to the "hack" state */ nextState(this); } /// \ingroup ServerProtocolFTPInternal static void ftpFail(FtpStateData *ftpState) { debugs(9, 6, HERE << "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") ); /* 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 */ switch (ftpState->state) { case SENT_CWD: case SENT_RETR: /* Try the / hack */ ftpState->hackShortcut(ftpTrySlashHack); return; default: break; } } ftpState->failed(ERR_NONE, 0); /* failed() closes ctrl.fd and frees this */ } void FtpStateData::failed(err_type error, int xerrno) { debugs(9,3,HERE << "entry-null=" << (entry?entry->isEmpty():0) << ", entry=" << entry); if (entry->isEmpty()) failedErrorMessage(error, xerrno); serverComplete(); } void FtpStateData::failedErrorMessage(err_type error, int xerrno) { ErrorState *ftperr; const char *command, *reply; /* Translate FTP errors into HTTP errors */ ftperr = NULL; switch (error) { case ERR_NONE: switch (state) { case SENT_USER: case SENT_PASS: if (ctrl.replycode > 500) if (password_url) ftperr = errorCon(ERR_FTP_FORBIDDEN, HTTP_FORBIDDEN, fwd->request); else ftperr = errorCon(ERR_FTP_FORBIDDEN, HTTP_UNAUTHORIZED, fwd->request); else if (ctrl.replycode == 421) ftperr = errorCon(ERR_FTP_UNAVAILABLE, HTTP_SERVICE_UNAVAILABLE, fwd->request); break; case SENT_CWD: case SENT_RETR: if (ctrl.replycode == 550) ftperr = errorCon(ERR_FTP_NOT_FOUND, HTTP_NOT_FOUND, fwd->request); break; default: break; } break; case ERR_READ_TIMEOUT: ftperr = errorCon(error, HTTP_GATEWAY_TIMEOUT, fwd->request); break; default: ftperr = errorCon(error, HTTP_BAD_GATEWAY, fwd->request); break; } if (ftperr == NULL) ftperr = errorCon(ERR_FTP_FAILURE, HTTP_BAD_GATEWAY, fwd->request); ftperr->xerrno = xerrno; ftperr->ftp.server_msg = ctrl.message; ctrl.message = NULL; if (old_request) command = old_request; else command = ctrl.last_command; if (command && strncmp(command, "PASS", 4) == 0) command = "PASS "; if (old_reply) reply = old_reply; else reply = ctrl.last_reply; if (command) ftperr->ftp.request = xstrdup(command); if (reply) ftperr->ftp.reply = xstrdup(reply); entry->replaceHttpReply( ftperr->BuildHttpReply() ); errorStateFree(ftperr); } /// \ingroup ServerProtocolFTPInternal static void ftpSendReply(FtpStateData * ftpState) { ErrorState *err; int code = ftpState->ctrl.replycode; http_status http_code; err_type err_code = ERR_NONE; debugs(9, 3, HERE << ftpState->entry->url() << ", code " << code); if (cbdataReferenceValid(ftpState)) debugs(9, 5, HERE << "ftpState (" << ftpState << ") is valid!"); if (code == 226 || code == 250) { err_code = (ftpState->mdtm > 0) ? ERR_FTP_PUT_MODIFIED : ERR_FTP_PUT_CREATED; http_code = (ftpState->mdtm > 0) ? HTTP_ACCEPTED : HTTP_CREATED; } else if (code == 227) { err_code = ERR_FTP_PUT_CREATED; http_code = HTTP_CREATED; } else { err_code = ERR_FTP_PUT_ERROR; http_code = HTTP_INTERNAL_SERVER_ERROR; } err = errorCon(err_code, http_code, ftpState->request); if (ftpState->old_request) err->ftp.request = xstrdup(ftpState->old_request); else err->ftp.request = xstrdup(ftpState->ctrl.last_command); if (ftpState->old_reply) err->ftp.reply = xstrdup(ftpState->old_reply); else if (ftpState->ctrl.last_reply) err->ftp.reply = xstrdup(ftpState->ctrl.last_reply); else err->ftp.reply = xstrdup(""); ftpState->entry->replaceHttpReply( err->BuildHttpReply() ); errorStateFree(err); ftpSendQuit(ftpState); } void FtpStateData::appendSuccessHeader() { const char *mime_type = NULL; const char *mime_enc = NULL; String urlpath = request->urlpath; const char *filename = NULL; const char *t = NULL; StoreEntry *e = entry; debugs(9, 3, HERE); if (flags.http_header_sent) return; HttpReply *reply = new HttpReply; flags.http_header_sent = 1; assert(e->isEmpty()); EBIT_CLR(e->flags, ENTRY_FWD_HDR_WAIT); e->buffer(); /* released when done processing current data payload */ filename = (t = urlpath.rpos('/')) ? t + 1 : urlpath.termedBuf(); if (flags.isdir) { mime_type = "text/html"; } else { switch (typecode) { case 'I': mime_type = "application/octet-stream"; mime_enc = mimeGetContentEncoding(filename); break; case 'A': mime_type = "text/plain"; break; default: mime_type = mimeGetContentType(filename); mime_enc = mimeGetContentEncoding(filename); break; } } /* set standard stuff */ if (0 == getCurrentOffset()) { /* Full reply */ reply->setHeaders(HTTP_OK, "Gatewaying", mime_type, theSize, mdtm, -2); } else if (theSize < getCurrentOffset()) { /* * DPW 2007-05-04 * offset should not be larger than theSize. We should * not be seeing this condition any more because we'll only * send REST if we know the theSize and if it is less than theSize. */ debugs(0,DBG_CRITICAL,HERE << "Whoops! " << " current offset=" << getCurrentOffset() << ", but theSize=" << theSize << ". assuming full content response"); reply->setHeaders(HTTP_OK, "Gatewaying", mime_type, theSize, mdtm, -2); } else { /* Partial reply */ HttpHdrRangeSpec range_spec; range_spec.offset = getCurrentOffset(); range_spec.length = theSize - getCurrentOffset(); reply->setHeaders(HTTP_PARTIAL_CONTENT, "Gatewaying", mime_type, theSize - getCurrentOffset(), mdtm, -2); httpHeaderAddContRange(&reply->header, range_spec, theSize); } /* additional info */ if (mime_enc) reply->header.putStr(HDR_CONTENT_ENCODING, mime_enc); setVirginReply(reply); adaptOrFinalizeReply(); } void FtpStateData::haveParsedReplyHeaders() { ServerStateData::haveParsedReplyHeaders(); StoreEntry *e = entry; e->timestampsSet(); if (flags.authenticated) { /* * Authenticated requests can't be cached. */ e->release(); } else if (EBIT_TEST(e->flags, ENTRY_CACHABLE) && !getCurrentOffset()) { e->setPublicKey(); } else { e->release(); } } HttpReply * FtpStateData::ftpAuthRequired(HttpRequest * request, const char *realm) { ErrorState *err = errorCon(ERR_CACHE_ACCESS_DENIED, HTTP_UNAUTHORIZED, request); HttpReply *newrep = err->BuildHttpReply(); errorStateFree(err); /* add Authenticate header */ newrep->header.putAuth("Basic", realm); return newrep; } /** \ingroup ServerProtocolFTPAPI \todo Should be a URL class API call. * * Construct an URI with leading / in PATH portion for use by CWD command * possibly others. FTP encodes absolute paths as beginning with '/' * after the initial URI path delimiter, which happens to be / itself. * This makes FTP absolute URI appear as: ftp:host:port//root/path * To encompass older software which compacts multiple // to / in transit * We use standard URI-encoding on the second / making it * ftp:host:port/%2froot/path AKA 'the FTP %2f hack'. */ const char * ftpUrlWith2f(HttpRequest * request) { String newbuf = "%2f"; if (request->protocol != PROTO_FTP) return NULL; 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); } return urlCanonical(request); } void FtpStateData::printfReplyBody(const char *fmt, ...) { va_list args; va_start (args, fmt); static char buf[4096]; buf[0] = '\0'; vsnprintf(buf, 4096, fmt, args); writeReplyBody(buf, strlen(buf)); } /** * Call this when there is data from the origin server * which should be sent to either StoreEntry, or to ICAP... */ void FtpStateData::writeReplyBody(const char *dataToWrite, size_t dataLength) { debugs(9, 5, HERE << "writing " << dataLength << " bytes to the reply"); addVirginReplyBody(dataToWrite, dataLength); } /** * called after we wrote the last byte of the request body */ void FtpStateData::doneSendingRequestBody() { debugs(9,3, HERE); dataComplete(); /* NP: RFC 959 3.3. DATA CONNECTION MANAGEMENT * if transfer type is 'stream' call dataComplete() * otherwise leave open. (reschedule control channel read?) */ } /** * A hack to ensure we do not double-complete on the forward entry. * \todo FtpStateData logic should probably be rewritten to avoid * double-completion or FwdState should be rewritten to allow it. */ void FtpStateData::completeForwarding() { if (fwd == NULL || flags.completed_forwarding) { debugs(9, 3, HERE << "completeForwarding avoids " << "double-complete on FD " << ctrl.fd << ", Data FD " << data.fd << ", this " << this << ", fwd " << fwd); return; } flags.completed_forwarding = true; ServerStateData::completeForwarding(); } /** * Close the FTP server connection(s). Used by serverComplete(). */ void FtpStateData::closeServer() { debugs(9,3, HERE << "closing FTP server FD " << ctrl.fd << ", Data FD " << data.fd << ", this " << this); if (ctrl.fd > -1) { fwd->unregister(ctrl.fd); ctrl.close(); } data.close(); } /** * Did we close all FTP server connection(s)? * \retval true Both server control and data channels are closed. \retval false Either control channel or data is still active. */ bool FtpStateData::doneWithServer() const { return ctrl.fd < 0 && data.fd < 0; } /** * 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. */ bool FtpStateData::haveControlChannel(const char *caller_name) const { if (doneWithServer()) return false; /* doneWithServer() only checks BOTH channels are closed. */ if (ctrl.fd < 0) { debugs(9, DBG_IMPORTANT, "WARNING! FTP Server Control channel is closed, but Data channel still active."); debugs(9, 2, caller_name << ": attempted on a closed FTP channel."); return false; } return true; } /** * Quickly abort the transaction * \todo destruction should be sufficient as the destructor should cleanup, * including canceling close handlers */ void FtpStateData::abortTransaction(const char *reason) { debugs(9, 3, HERE << "aborting transaction for " << reason << "; FD " << ctrl.fd << ", Data FD " << data.fd << ", this " << this); if (ctrl.fd >= 0) { comm_close(ctrl.fd); return; } fwd->handleUnregisteredServerEnd(); deleteThis("FtpStateData::abortTransaction"); } /// creates a data channel Comm close callback AsyncCall::Pointer FtpStateData::dataCloser() { typedef CommCbMemFunT Dialer; return JobCallback(9, 5, Dialer, this, FtpStateData::dataClosed); } /// configures the channel with a descriptor and registers a close handler void FtpChannel::opened(int aFd, const AsyncCall::Pointer &aCloser) { assert(fd < 0); assert(closer == NULL); assert(aFd >= 0); assert(aCloser != NULL); fd = aFd; closer = aCloser; comm_add_close_handler(fd, closer); } /// planned close: removes the close handler and calls comm_close void FtpChannel::close() { if (fd >= 0) { comm_remove_close_handler(fd, closer); closer = NULL; comm_close(fd); // we do not expect to be called back fd = -1; } } /// just resets fd and close handler void FtpChannel::clear() { fd = -1; closer = NULL; }