/* * Copyright (c) 2000-2001 Apple Computer, Inc. All Rights Reserved. * * The contents of this file constitute Original Code as defined in and are * subject to the Apple Public Source License Version 1.2 (the 'License'). * You may not use this file except in compliance with the License. Please obtain * a copy of the License at http://www.apple.com/publicsource and read it before * using this file. * * This Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the * specific language governing rights and limitations under the License. */ // // ftp-protocol - FTP protocol objects // // Basic design notes: // FTPConnection and FTPDataConnection are mildly incestuous. An FTPConnection // *contains* an FTPDataConnection to manage its data channel during transfers. // It could *be* an FTPDataConnection, but they are both TransferEngine::TCPClients, // which would make coding awkward and mistake prone. // During wrap-up of a transfer, the control and data channels must synchronize to // make sure they're both done. (Note that 226/250 replies do NOT guarantee that all // data has been received on the data path; network latency can hold back that data // for an arbitrarily long time (modulo TCP timeouts). Synchronization is achieved in // classic ping-pong fashion: FTPConnection calls FTPDataConnection::connectionDone() // to signal that it's side is done. The data connection calls FTPConnection::finish once // it knows they're both done (because FTPConnection told it about its side already). // // This version has support for simple FTP proxy operation, where the PASS argument // is of the form user@remote-host. FTPProxyProtocol uses this support to implement // FTP/FTP proxies. // // Limits on functionality: // Only stream mode is supported. // No EBCDIC support. // #include #include "ftp-protocol.h" #include "netparameters.h" namespace Security { namespace Network { // // Construct the protocol object // FTPProtocol::FTPProtocol(Manager &mgr) : Protocol(mgr, "ftp") { } // // Create a Transfer object for our protocol // FTPProtocol::FTPTransfer *FTPProtocol::makeTransfer(const Target &target, Operation operation) { return new FTPTransfer(*this, target, operation); } // // Construct an FTPConnection object // FTPProtocol::FTPConnection::FTPConnection(Protocol &proto, const HostTarget &hostTarget) : TCPConnection(proto, hostTarget), state(errorState), mImageMode(false), mDataPath(*this) { const HostTarget &host = proxyHostTarget(); connect(host.host(), host.port()); state = loginInitial; } // // Issue a request on the connection. // void FTPProtocol::FTPConnection::request(const char *path) { assert(isDocked()); mOperationPath = path; if (state == idle) // already (idly) at command prompt, so... startCommand(); // ... start operation right now } void FTPProtocol::FTPConnection::startCommand() { // notify any observer of the change in status observe(Observer::resourceFound); switch (operation()) { case makeDirectory: printfe("MKD %s", mOperationPath.c_str()); state = directCommandSent; return; case removeDirectory: printfe("RMD %s", mOperationPath.c_str()); state = directCommandSent; return; case removeFile: printfe("DELE %s", mOperationPath.c_str()); state = directCommandSent; return; case genericCommand: printfe("%s", mOperationPath.c_str()); state = directCommandSent; return; } // all other commands initiate data transfers. First, set appropriate mode bool wantImageMode; switch (operation()) { case downloadDirectory: case downloadListing: wantImageMode = false; break; case download: case upload: wantImageMode = getv(kNetworkFtpTransferMode, "I") == "I"; break; default: assert(false); } // adjust transfer mode if needed if (mImageMode != wantImageMode) { printfe("TYPE %s", wantImageMode ? "I" : "A"); mImageMode = wantImageMode; // a bit premature, but this shouldn't fail state = typeCommandSent; return; // we'll be back here } if (mPassive = getv(kNetworkFtpPassiveTransfers)) { // initiate passive mode download printfe("PASV"); state = passiveSent; } else { // initiate "active mode" (default mode) download. // The cooking recipe for the host/port address is deliciously subtle. We obviously take // the receiver's bound port. But in most cases, its address at this stage (passive bound) // is ANY, and thus useless to the server. We pick the command connection's local // address for completion. However, in SOME cases mReceiver.localAddress() has // a meaningful value (SOCKS, for one), so we allow this to prevail if available. mReceiver.open(); // open receiver and bind FTPAddress addr(mReceiver.localAddress().defaults(localAddress())); printfe("PORT %u,%u,%u,%u,%u,%u", addr.h1, addr.h2, addr.h3, addr.h4, addr.p1, addr.p2); state = portSent; } } // // Initiate a data transfer (any direction or form) as indicated by mOperation. // mDataPath has already been set up. // void FTPProtocol::FTPConnection::startTransfer(bool restarted) { if (!restarted) if (int restartOffset = getv(kNetworkRestartPosition, 0)) { // restart requested - insert a REST command here printfe("REST %d", restartOffset); state = restartSent; return; } switch (operation()) { case download: printfe("RETR %s", mOperationPath.c_str()); break; case downloadDirectory: printfe("NLST %s", mOperationPath.c_str()); break; case downloadListing: printfe("LIST %s", mOperationPath.c_str()); break; case upload: printfe("%s %s", getv(kNetworkFtpUniqueStores, false) ? "STOU" : "STOR", mOperationPath.c_str()); break; default: assert(false); } state = transferSent; } // // This is the master state transit machine for FTP. // void FTPProtocol::FTPConnection::transit(Event event, char *input, size_t length) { if (!isDocked()) { // not docked; event while in Connection cache abort(); // clean up return; } switch (event) { case connectionDone: // TCP connection complete or failed { int error = length; // transmitted in the 'length' argument observe(Observer::connectEvent, &error); if (error) // retry connect(); else // connection good mode(lineInput); } return; case inputAvailable: { restarting(false); // valid input observed, commit to this Connection // interpret input as FTP protocol reply, handling continued responses observe(Observer::protocolReceive, input); if (replyContinuation(input)) return; // still continuing, keep reading InetReply reply(input); // parse this reply if (!reply.valid()) // don't know why, but we're dead fail(input); if (replyContinuation(reply)) return; // is continuation now // dispatch state machine switch (state) { case loginInitial: switch (reply) { case 220: { string username = getv(kNetworkGenericUsername, hostTarget.haveUserPass() ? hostTarget.username() : "anonymous"); if (transfer().protocol.isProxy()) { char portPart[10]; sprintf(portPart, ":%d", transfer().target.host.port()); username += "@" + transfer().target.host.host().name() + portPart; } printfe("USER %s", username.c_str()); state = loginUserSent; break; } default: fail(input); } break; case loginUserSent: switch (reply) { case 331: { string password = getv(kNetworkGenericPassword, hostTarget.haveUserPass() ? hostTarget.password() : "anonymous@nowhere.net"); printfe("PASS %s", password.c_str()); state = loginPassSent; break; } case 230: startCommand(); break; default: fail(input); } break; case loginPassSent: switch (reply) { case 230: startCommand(); break; default: fail(input); } break; case typeCommandSent: switch (reply) { case 200: startCommand(); break; default: fail(input); } break; case passiveSent: switch (reply) { case 227: { // reply text =~ Entering passive mode (h1,h2,h3,h4,p1,p2) FTPAddress addr; if (const char *p = strchr(reply.message(), '(')) { if (sscanf(p, "(%u,%u,%u,%u,%u,%u)", &addr.h1, &addr.h2, &addr.h3, &addr.h4, &addr.p1, &addr.p2) != 6) fail(input); } else if (const char *p = strstr(reply.message(), "mode")) { // RFC1123 says to be really nice to BROKEN FTP servers here if (sscanf(p+4, "%u,%u,%u,%u,%u,%u", &addr.h1, &addr.h2, &addr.h3, &addr.h4, &addr.p1, &addr.p2) != 6) fail(input); } else { fail(input); return; } mDataPath.open(addr); //@@@ synchronous - move to state machine startTransfer(); } break; default: fail(input); } break; case portSent: switch (reply) { case 200: // PORT command successful startTransfer(); break; default: fail(input); } break; case restartSent: switch (reply) { case 350: // Restarting at ... startTransfer(true); // now do the transfer command for real break; default: fail(input); } break; case transferSent: switch (reply) { case 150: case 125: transfer().ftpResponse() = input; // remember response for caller. transfer().ftpResponseCode() = reply; if (!mPassive) mReceiver.receive(mDataPath); // accept incoming connection and stop listening observe(Observer::resultCodeReady, input); // engage the data path switch (operation()) { case download: case downloadDirectory: case downloadListing: mDataPath.start(sink()); observe(Observer::downloading, input); break; case upload: mDataPath.start(source()); observe(Observer::uploading, input); break; default: assert(false); } state = transferInProgress; break; default: // download command failed if (!mPassive) mReceiver.close(); state = idle; fail(); break; } break; case transferInProgress: switch (reply) { case 226: // transfer complete state = idle; // idle command mode retain(true); mDataPath.connectionDone(); break; case 452: mDataPath.close(); state = idle; fail(input, dskFulErr); break; default: // transfer failed // (ignore any error in mDataPath - prefer diagnostics from remote) mDataPath.close(); state = idle; fail(input); break; } break; case directCommandSent: { switch (reply.type()) { case 2: retain(true); finish(); break; default: fail(); break; } state = idle; } break; default: assert(false); } } break; case endOfInput: return restart(); // try to restart, fail if we can't (or shouldn't) default: assert(false); } } void FTPProtocol::FTPConnection::transitError(const CssmCommonError &error) { //@@@ need to do much better diagnostics here fail(); // fail transfer and discard connection } bool FTPProtocol::FTPConnection::validate() { assert(state == idle); tickle(); return state == idle; } // // The data connection object // void FTPProtocol::FTPDataConnection::start(Sink &sink) { debug("ftp", "data connection starts download"); setup(); mode(sink); } void FTPProtocol::FTPDataConnection::start(Source &source) { debug("ftp", "data connection starts upload"); setup(); mode(source); } void FTPProtocol::FTPDataConnection::setup() { connection.protocol.manager.addIO(this); mFailureStatus = noErr; // okay so far mConnectionDone = false; // connection side not ready yet mTransferDone = false; // our side not ready net } int FTPProtocol::FTPDataConnection::fileDesc() const { return *this; } void FTPProtocol::FTPDataConnection::transit(Event event, char *input, size_t length) { assert(event == autoReadDone || event == autoWriteDone || event == endOfInput); debug("ftp", "data transfer complete"); close(); // close data path finish(); // proceed with state protocol } void FTPProtocol::FTPDataConnection::transitError(const CssmCommonError &error) { mFailureStatus = error.osStatus(); close(); // close data path finish(); // proceed with state protocol } void FTPProtocol::FTPDataConnection::close() { if (isOpen()) { connection.protocol.manager.removeIO(this); TCPClientSocket::close(); mTransferDone = true; } } void FTPProtocol::FTPDataConnection::connectionDone() { mConnectionDone = true; finish(); } void FTPProtocol::FTPDataConnection::finish() { if (mFailureStatus) { connection.fail("data transfer failed", mFailureStatus); connection.finish(); } else if (mTransferDone && mConnectionDone) { connection.finish(); } else if (mConnectionDone) { debug("ftp", "holding for data transfer completion"); } else { debug("ftp", "holding for control message"); } } // // Transfer objects // FTPProtocol::FTPTransfer::FTPTransfer(Protocol &proto, const Target &tgt, Operation operation) : Transfer(proto, tgt, operation, defaultFtpPort) { } void FTPProtocol::FTPTransfer::start() { FTPConnection *connection = protocol.manager.findConnection(target); if (connection == NULL) connection = new FTPConnection(protocol, target); connection->dock(this); connection->request(target.path.c_str()); } void FTPProtocol::FTPTransfer::abort() { observe(Observer::aborting); setError("aborted"); connectionAs().abort(); } void FTPProtocol::FTPConnection::abort() { close(); mDataPath.close(); fail(); } Transfer::ResultClass FTPProtocol::FTPTransfer::resultClass() const { switch (state()) { case failed: { InetReply reply(errorDescription().c_str()); if (reply / 10 == 53) // 53x - authentication failure return authorizationFailure; if (errorDescription() == "aborted") return abortedFailure; // when in doubt, blame the remote return remoteFailure; } case finished: return success; default: assert(false); } } // // Translate the ideosyncratic text form of FTP's socket addresses to and from the real thing // FTPProtocol::FTPAddress::FTPAddress(const IPSockAddress &sockaddr) { uint32 addr = sockaddr.address(); h1 = addr >> 24; h2 = (addr >> 16) & 0xFF; h3 = (addr >> 8) & 0xFF; h4 = addr & 0xFF; p1 = sockaddr.port() >> 8; p2 = sockaddr.port() & 0xFF; } FTPProtocol::FTPAddress::operator IPSockAddress() const { assert(!(h1 & ~0xff) & !(h2 & ~0xff) & !(h3 & ~0xff) & !(h4 & ~0xff) & !(p1 & ~0xff) & !(p2 & ~0xff)); return IPSockAddress(IPAddress(h1 << 24 | h2 << 16 | h3 << 8 | h4), p1 << 8 | p2); } } // end namespace Network } // end namespace Security