// sftpd.cpp // secure ftp proxy daemon // copyright SafeTP Development Group, Inc., 2000 Terms of use are as specified in license.txt #include "str.h" // string #include // printf #include // strlen #include // atoi #include // isdigit, isprint #include // signal #include // errno #include // time, localtime, strftime #include // struct tm #include "socket.h" // socket funcs #include "sockutil.h" // utilities #include "exc.h" // exceptions #include "breaker.h" // breaker #include "missing.h" // missing_stricmp #include "nonport.h" // portableSleep #include "syserr.h" // xSysError #include "strtokp.h" // StrtokParse #include "reply.h" // Reply #include "request.h" // Request #include "datablok.h" // DataBlock #include "provider.h" // SecurityProvider #include "base64t.h" // Base64Transform #include "globrand.h" // {read,save}RandomSeed #include "keyutils.h" // sm_testKey #include "hcl.h" // inetdStartupHook #ifdef __UNIX__ # include // openlog, syslog #endif #include "sftpver.h" // SFTP_version #include "sftpd.h" // this module // defined in sdsa.cc (this introduces a dependency I don't // like, so I'm hesitant to simply include sdsa.h) void knownDSAPublicKeyVersions(int &minimum, int &maximum); // for now, 959 compatibility is critical for deployment #ifndef DEFAULT_ALLOW_RFC959 # define DEFAULT_ALLOW_RFC959 1 #endif // it probably doesn't matter whether nonreflexive is // allowed or not; anyway it seems unlikely it could be made // to work on the client side(s), because of the necessary // shared security information #ifndef DEFAULT_ALLOW_NONREFLEXIVE_PORT # define DEFAULT_ALLOW_NONREFLEXIVE_PORT 1 #endif // it seems like a good idea to not let people use this // service to, e.g., send mail via SMTP... #ifndef DEFAULT_ALLOW_RESERVED_PORT # define DEFAULT_ALLOW_RESERVED_PORT 0 #endif // allow diagnostic messages to be compiled away, so even the cost // of producing the log message in memory is eliminated #ifndef ALLOW_DIAGNOSTIC_LOGS # define ALLOW_DIAGNOSTIC_LOGS 1 #endif // convenient forms for calling the log function #define log(event, expr) writeToLog(LL_LOG, event, stringb(expr)) #if ALLOW_DIAGNOSTIC_LOGS # define diagnostic(expr) \ writeToLog(LL_DIAGNOSTIC, LE_DEBUG, stringb(expr)) # define socket_diagnostic(expr) \ writeToLog(LL_SOCKET_DIAG, LE_DEBUG, stringb(expr)) # define diagnosticNoLF(expr) \ writeToLog((LogLevel)(LL_DIAGNOSTIC | LL_NO_LF), LE_DEBUG, stringb(expr)) #else # define diagnostic(expr) ((void)0) # define socket_diagnostic(expr) ((void)0) # define diagnosticNoLF(expr) ((void)0) #endif // for debugging (see below) bool attachTrigger; void waitForAttach() { // wait for debugger to attach and set trigger attachTrigger = false; while (!attachTrigger) { portableSleep(1); breaker(); // so we just set a bkpoint at usual spot to catch this } } //----------------------------- bool SFTPD::Is959Allowed(char const *name) { bool found = false; char testname[100]; char *ep; // UserName部分をチェックしてるところと思われる部分 // 改良不可? //allow959File = fopen("c:\\Net\\SafeTPd\\allow_959.txt", "r"); if (allow959File) { fseek(allow959File, 0, SEEK_SET); while (!feof(allow959File) && !found) { fgets(testname, sizeof(testname), allow959File); if (*testname == '#') continue; ep = testname + strlen(testname) - 1; if (*ep == '\n') *ep = 0; found = !strcmp(name, testname); } //fclose(allow959File); } return found; } // IsDropAllowed bool SFTPD::IsIPAllowed(char const *name) { bool found = false; char testname[200]; char *ep; unsigned b1,b2,b3,b4,msk; IPAddress testip, testmask; // dword // CIDR部分をチェックしてるところと思われる部分 // 全面的に改良する // allowIPFile = fopen("c:\\Net\\SafeTPd\\allow_ip.txt", "r"); if (allowIPFile) { fseek(allowIPFile, 0, SEEK_SET); while (!feof(allowIPFile) && !found) { fgets(testname, sizeof(testname), allowIPFile); if (*testname == '#') continue; if (!(ep = strchr(testname, ' '))) continue; *ep = 0; ep++; if ((testname[0]=='*' && testname[1]==0) || !strcmp(name, testname)) { if (sscanf(ep, "%u.%u.%u.%u/%u", &b1,&b2,&b3,&b4,&msk) != 5) continue; if (b1>255 || b2>255 || b3>255 || b4>255 || msk>32) continue; testip = (IPAddress)b1<<24 | (IPAddress)b2<<16 | (IPAddress)b3<<8 | (IPAddress)b4; testmask = msk ? 0xFFFFFFFF << (32-msk) : 0; if ((relayClientAddress & testmask) == testip) { found = true; *(--ep) = ' '; ep = testname + strlen(testname) - 1; if (*ep == '\n') *ep = 0; log(LE_ERROR, "allowed \"" << name << "\" from " << formatAddress(relayClientAddress) << ", rule: " << testname); } break; } } // while //fclose(allowIPFile); } if (!found) log(LE_ERROR, "denied " << name << " from " << formatAddress(relayClientAddress)); return found; } // IsDropAllowed // --------------------- general stuff ----------------------------- // this is used by the warning handler, and obviously its presence // means it isn't thread-safe to make multiple instances of SFTPD... SFTPD *SFTPD::instance = NULL; int main(int argc, char *argv[]) { // having troubles... //waitForAttach(); // immediately hand off to an SFTPD function so we are // in its scope SFTPD sftpd; sftpd.run(argc, argv); breaker(); // breakpoint return 0; } void SFTPD::run(int argc, char *argv[]) { try { # ifdef __UNIX__ // setup syslog logging openlog("sftpd", LOG_PID, LOG_DAEMON); # endif // capture 'warn' module messages warningHandler = SFTPD::warnHandler; // setup nonport error handling nonportFail = xSysError::xsyserror; // socket api initialization socket_lib_init(); # ifdef SIGPIPE // ignore broken-pipe signals signal(SIGPIPE, SIG_IGN); # endif // run the main code innerRun(argc, argv); } catch (xBase &x) { breaker(); // breakpoint log(LE_ERROR, "Exception caught: " << x.why()); } diagnostic("sftpd exiting, " << numSocketsOpen() << " sockets leaked"); // we used to be saving the random seed here, but now we // use the RandomSeedSaver helper class } PortRange::PortRange() : restricted(false), low(0), high(0) {} // there should be an entry here for *every* data member, even // if that member doesn't actually need an initialization SFTPD::SFTPD() : // ------ socket state ------ useStdinStream(false), client_control(INVALID_SOCKET), clientControlStream(NULL), server_control(INVALID_SOCKET), serverControlStream(NULL), client_data(INVALID_SOCKET), server_data(INVALID_SOCKET), client_listen(INVALID_SOCKET), server_listen(INVALID_SOCKET), // ------ protocol state ------ state(STATE_UNCONNECTED), relayClientAddress(INADDR_NONE), relayClientPort(0), serverPassivePort(NONPASV_PORT), digt(), // ------ data channel state ------ dataBuffer(), // not allocated maxBlockSize(0), maxCleartextBlockSize(0), forceDataRelay(false), dataSecLevel(DSL_CLEAR), // until we see PROT // ------ configuration options ------ allowRfc959(DEFAULT_ALLOW_RFC959), allowRfc959_anon(false), allowNonReflexivePorts(DEFAULT_ALLOW_NONREFLEXIVE_PORT), allowReservedPorts(DEFAULT_ALLOW_RESERVED_PORT), allowCleartext(false), requireDataEncryption(false), useFakeIPAddress(false), // for -i fakeIPAddress(0), // for -i pasvRange(), activeRange(), kerberosFtpdBinary((char*)NULL), anonExecDropdown(false), anonFtpdBinary((char*)NULL), printAdats(false), printDataChannel(false), artificialDelay(0), logLevelMask(LL_LOG), stdoutLogAnyway(false), logEventMask(LE_DEFAULTS), logFile(stderr), security(NULL) { instance = this; allowIPFile = NULL; allow959File = NULL; } SFTPD::~SFTPD() { // attempt to close sockets (don't care about errors here) //try { closeSocket(client_control); } catch (...) {} //try { closeSocket(server_control); } catch (...) {} // update: don't bother because the main driver loop really // should handle it, and that way I don't close something twice // dealloc streams if (clientControlStream) { delete clientControlStream; } if (serverControlStream) { delete serverControlStream; } // dealloc security if (security) { delete security; } // close log file if (logFile != stderr) { fclose(logFile); } // prevent further access via 'instance' if (instance == this) { instance = NULL; } } // this is a convenient way to make sure the randomSeed gets // saved whenever it has been read SFTPD::RandomSeedSaver::~RandomSeedSaver() { try { saveRandomSeed(); } catch (...) { sftpd->writeToLog(SFTPD::LL_LOG, LE_ERROR, "failed to write random seed"); } } // f-ing egcs-1.1.2 wants to piss on the floor about possibly // uninitalized variables if this code appears below, where it // should, so I'm (very disgruntledly) pulling it up here static unsigned long f_ing_strtoul(char const *str, int radix) { char *endptr; return strtoul(str, &endptr, radix); } void SFTPD::innerRun(int argc, char *argv[]) { // ----------- initial setup -------------- // see if getservbyname has a value (only used when not started by inetd) // this port and address are what get bound in to a listener socket int sftpdPort = getServByName("safetp"); IPAddress sftpdInterface = INADDR_ANY; // by default, bind all interfaces // the ftp port is tricky.. int ftpdPort = getServByName("raw-ftp"); IPAddress ftpdAddress = INADDR_ANY; // dummy value // by default forward to same interface the connection is received on bool ftpdForwardNonlocally = false; // use hardcoded values if getservbyname doesn't help if (sftpdPort == 0) { sftpdPort = SFTPD_PORT; } if (ftpdPort == 0) { ftpdPort = FTPD_PORT; } // in case we see an exception before we know where to log them xSysError *delayedError = NULL; # ifdef __WIN32__ SOCKET theStdin = INVALID_SOCKET; // e.h. scope workaround xSysError *firsterr = NULL; xSysError *seconderr = NULL; // the startup hook functions may return INVALID_SOCKET // if they can easily determine their inetd is not the spawning inetd // they may also throw an xSysError if there was an error contacting their inetd try { theStdin = inetdStartupHookWinetd(argc, argv); // try to load Winetd inetd } catch (xSysError &e) { firsterr = new xSysError(e); // throw it again later } if (theStdin == INVALID_SOCKET) { // failed to load winetd, try HCL try { theStdin = inetdStartupHookHCL(argc, argv); // try to load hummingbird inetd } catch (xSysError &e) { seconderr = new xSysError(e); // throw it again later } } if (theStdin != INVALID_SOCKET) { // we're using one of our inetd's so force using this socket useStdinStream = true; } else if (firsterr || seconderr) { // there was some error reported // we need to set a delayed error that includes the failed state from one or both inetd's // (because we're not certain which they're intending to use) string combinedreason = stringb(""); if (firsterr) { // winetd failed combinedreason = stringb(combinedreason << "Winetd: " << firsterr->why() << " "); delete firsterr; } if (seconderr) { // winetd failed combinedreason = stringb(combinedreason << "HCLInetd: " << seconderr->why() << " "); delete seconderr; } delayedError = new xSysError(xSysError::R_UNKNOWN, "sftpd failed to contact inetd software.", combinedreason); } # else SOCKET theStdin = (SOCKET)0; // unix stdin # endif // process command line { bool errors = false; for (int a=1; a= 2) { logLevelMask = (LogLevel)(logLevelMask | LL_SOCKET_DIAG); } if (d >= 3) { printDataChannel = true; } } break; case 'a': // print ADATs printAdats = true; break; case 'l': { // set log file FILE *fp = fopen(argString, "a"); if (!fp) { log(LE_ERROR, "failed to open log file " << argString); errors = true; return; } else { if (logFile != stderr) { fclose(logFile); } logFile = fp; // it's silly to require that people specify -o with -l stdoutLogAnyway = true; } break; } case 'm': logEventMask = (LoggableEvent) f_ing_strtoul(argv[a]+2, 0 /*auto-radix*/); break; //CIDR方式のIPチェック部 //ここを 192.168.1.* や *.hogehoge.com などのフォーマットに対応させるように改良予定 case '6': { // set ip allow file FILE *fp = fopen(argString, "r"); if (!fp) { log(LE_ERROR, "failed to open ip allow file " << (argString)); return; } else { if (allowIPFile) { fclose(allowIPFile); } allowIPFile = fp; } break; } //ユーザネームチェック部 case '7': { // set dropdown allow file allowRfc959 = false; allowRfc959_anon = false; FILE *fp = fopen(argString, "r"); if (!fp) { log(LE_ERROR, "failed to open dropdown allow file " << (argString)); return; } else { if (allow959File) { fclose(allow959File); } allow959File = fp; } break; } case 'x': if (requireDataEncryption) { // handle -c before -x log(LE_ERROR, "-x is incompatible with -c"); errors = true; } else { allowCleartext = true; } break; case '9': allowRfc959 = false; allowRfc959_anon = false; break; case '8': // like above, but we do allow dropdown for anon allowRfc959 = false; allowRfc959_anon = true; break; case 't': waitForAttach(); break; case 'y': if (!changeDirectory(argString)) { log(LE_ERROR, "failed to chdir to " << argString); errors = true; } break; case 'K': kerberosFtpdBinary = argString; break; case 'e': anonExecDropdown = true; anonFtpdBinary = argString; break; case 'i': // send a different address when authenticating; // for a server sitting behind an address-translating firewall fakeIPAddress = resolveHostName(argString); useFakeIPAddress = true; break; case 'c': requireDataEncryption = true; // 9/22/00 02:07: tightening-down -c even more: make it // imply -3, so the direct-to-ftpd route is completely // closed, and -9 so we never accept unencrypted conns forceDataRelay = true; allowRfc959 = false; allowRfc959_anon = false; allowCleartext = false; // in case -c after -x break; case 'r': case 'R': { // parse low/high StrtokParse tok(argString, "-"); if (tok != 2) { log(LE_ERROR, "argument to -" << argv[a][1] << " is malformed"); errors = true; break; } int low = atoi(tok[0]); int high = atoi(tok[1]); // may as well sanity-check the values; don't let them // be 0 because that is atoi's error return if (!( 1 <= low && low <= 0xFFFF && 1 <= high && high <= 0xFFFF && low <= high )) { log(LE_ERROR, "argument(s) to -" << argv[a][1] << " malformed or out of range [1,65535]"); errors = true; break; } // stash the range in the proper variable if (argv[a][1] == 'r') { pasvRange.setRange(low, high); } else { activeRange.setRange(low, high); } break; } case 'h': // purpose of defining this is to ensure we reserve // an option for printing the help text // (dropthru to next case) default: oneError = true; break; } } if (oneError) { // "-h" is treated like an error in most respects, // but shouldn't generate an actual error message if (!( argv[a][0] == '-' && argv[a][1] == 'h' )) { // potential problem here: errors in inetd.conf will // not cause visible error messages... //fprintf(stderr, "error in argument: %s\n", argv[a]); // possibly addressed; if the log output file is specified // early, then the user will see later errors log(LE_ERROR, "error in argument: " << argv[a]); } // cause usage to be printed and program to exit errors = true; } } // see if we had any trouble before processing command line if (delayedError) { xSysError temp(*delayedError); delete delayedError; // such a good little soldier! (Dan was making fun of me for this :) ) THROW(temp); } // print usage if any argument errors if (errors) { fprintf(stderr, "usage: sftpd [options]\n" " general options:\n" " -v print version\n" " -h print this message\n" " -y specify directory where keys are (default is cwd)\n" " control channel:\n" " -s use stdin as control connection (for use with inetd)\n" " -p listen for incoming connections on port N (default: %d)\n" " -p: listen on interface ADDR (dotted decimal) and port N\n" " -f contact ftpd on port N (default: %d)\n" " -f: contact ftpd at another host (ADDR) and port N;\n" " (this is potentially INSECURE -- see sftpd.html)\n" " data channel:\n" " -c require all data transfers to be encrypted\n" " -r- restrict ports used for PASV to range [low,high]\n" " -R- restrict ports used for PORT to range [low,high]\n" " logging:\n" " -l log to a file instead of stdout or syslog\n" " -o for -s, send log output to stdout instead of syslog\n" " -d1 log diagnostic (debugging) messages\n" " -d2 .. log even more detailed diagnostic messages\n" " -d3 .. and print all data channel traffic to stdout\n" " -a log ADATs\n" " -m set mask of events to log -- see sftpd.html\n" " compatibility:\n" " -9 disallow RFC 959 (unencrypted) connections\n" " -8 disallow RFC 959 unless the user is anonymous\n" " -7 allow RFC 959 connections for users listed in file\n" " -6 allow connections only from certain users/IPs\n" " -3 disable 3rd-party transfer optimization\n" " -i
use alternate server address for authenication\n" " -K Kerberos compatibility (specify kftpd image)\n" " -e Handle anon ftp by exec'ing this ftpd to handle it\n" //" -x allow X-Cleartext and X-Cleartext2 protocols\n" //" -t start and immediately wait for debugger to attach\n" //" -Dn sleep n seconds between data blocks (simulates slow link)\n" " for inetd, typical arguments are -s -y/home/safetp -f351\n", sftpdPort, ftpdPort); return; } } if (anonExecDropdown && !useStdinStream) { log(LE_ERROR, "error: can only use the -e switch when run from inetd (-s switch)"); // this is because the exec'd ftpd will expect sockets on stdin and // stdout, but if we're not run from inetd, then stdin and stdout // won't even be sockets, let alone the right ones return; } if (ftpdPort == sftpdPort && !ftpdForwardNonlocally) { log(LE_WARNING, "warning: sftpd may get into an infinite loop; is " << ftpdPort << " the right port to contact? (this warning " "is triggered by ftpdPort==sftpdPort)"); } if (useStdinStream) { // we don't want throwing exceptions to write to stderr because // that is now connected to the control channel (could redirect // it, but we probably don't want the exception-thrown records // in the log file at all at this stage of development) xBase::logExceptions = false; } if (logLevelMask & LL_DIAGNOSTIC) { // turn on digest echoes DigestComputer::echoDigestInput = true; } // read seed; we wait until now because we don't want to risk // generating log output until the -s option, if present, has // been processed readRandomSeed(); // and now, we want to make sure we will write the updated version, // to avoid replay potential RandomSeedSaver seedSaver(this); // Dan and I both ran into situations where it would have been nice // for sftpd to report missing keys earlier.. so let's try to do that if (!sm_testKey("DSA/public.key", true) || !sm_testKey("DSA/private.key", true)) { log(LE_ERROR, "ERROR: at least one of the DSA keys is missing"); // even though in theory there is something useful sftpd can do // without its keys (namely, test X-CLEARTEXT), that's a very rare // case; to avoid confusion, bail return; } // ----------- listen for a new connection -------------- if (useStdinStream) { // we are already connected client_control = theStdin; // file descriptor 0 is stdin; note that INVALID_SOCKET is -1, // so we aren't going to collide with the error value incOpenSockets(); // since we inherited this socket, rather than opened in directly, // we have to increment this manually } else { // NOTE: we must run as root here, if sftpdPort is < 1024 // establish a listener on the sftpd port SOCKET listener = interface_listen_socket(sftpdInterface, sftpdPort); addEntropy(); // wait for a connection if (sftpdInterface == INADDR_ANY) { log(LE_CONTROL_CHANNEL, "Waiting for connection to sftpd on port " << sftpdPort << "..."); } else { log(LE_CONTROL_CHANNEL, "Waiting for connection to sftpd on interface " << formatAddress(sftpdInterface, sftpdPort) << "..."); } client_control = accept_socket(listener); addEntropy(); // don't need listener any longer closeSocket(listener); // TODO: some people do run sftpd as root; it would be good to // drop root privileges here } // begin: exceptions should cause a protocol message try { // set the default client data transfer port relayClientAddress = getRemoteAddress(client_control); relayClientPort = getRemotePort(client_control); // print connection information log(LE_CONTROL_CHANNEL, "received connection from host " << formatAddress(relayClientAddress, relayClientPort)); // ----------- connect to ftpd -------------- // it appears that typical ftpd implementations do not allow // arbitrary PORT command IP addresses when their control // connection is to localhost (127.0.0.1); therefore, we find out // which IP address the client connected to, and use that IP when // connecting to ftpd (unless an alternate has been supplied with // -f); this is necessary only when data channel protection is // *off*, because in that situation we let the client's PORT // commands through unmodified // decide which address to use for contacting ftpd if (!ftpdForwardNonlocally) { // use same interface that client connected to ftpdAddress = getLocalAddress(client_control); } else { // user has already specified it, and it is stored // in 'ftpdAddress' } // open connection to real ftp server diagnostic("connecting to ftpd at " << formatAddress(ftpdAddress, ftpdPort) << "..."); server_control = connect_socket(ftpdAddress, ftpdPort); addEntropy(); diagnostic("connected to ftpd"); // display socket info socket_diagnostic("Control connection to client: " << sockInfo(client_control)); socket_diagnostic("Control connection to ftpd: " << sockInfo(server_control)); // ----------- initial relay -------------- // construct buffering layer on server socket serverControlStream = new SocketLineReader(server_control); // annotate initial response from the server bool readReplyOk = false; try { Reply reply(*serverControlStream); readReplyOk = true; addEntropy(); reply.append("*** This server can accept secure (encrypted) connections. ***"); reply.append("*** See http://safetp.cs.berkeley.edu for info. ***"); send(reply, client_control); } catch (xBase &) { if (!readReplyOk) { // the most common cause is the exception reported by reply.cpp about reading // past the end of the stream, caused by the server accepting the connection // but immediately closing it without sending a greeting xfailure("FTP server immediately closed connection; it's probably disabled"); } else { // something else; handle normally throw; } } // new feature: anon dropdown via exec if (anonExecDropdown) { possiblyDoAnonExecDropdown(client_control); } // only *after* the possible anon dropdown stuff do we // setup buffering on the client's control channel clientControlStream = new SocketLineReader(client_control); // modify session state state = STATE_UNAUTHENTICATED; } catch (xBase &x) { // report error to user, via protocol stream unexpected(x); // the error has been adequately reported; just bail return; } // ----------- main response loop -------------- // enter request-reply loop for(;;) { // we'll record which one socket being used, for // processing during catch-all catch statement SOCKET *active = NULL; try { // bugfix for my 226 nemesis: we might have a *complete* // response buffered in a StreamLineReader, and if so, select() // won't tell us if (clientControlStream->hasUnprocessedData()) { socket_diagnostic("Unprocessed data on client control channel"); // NOTE: this code is repeated below active = &client_control; handleClientRequest(); continue; } if (serverControlStream->hasUnprocessedData()) { socket_diagnostic("Unprocessed data on server control channel"); // NOTE: this code is repeated below active = &server_control; handleServerReply(); continue; } // add all sockets we have; if any are INVALID_SOCKET, sockset // will simply ignore them SocketSet sockset; sockset.add(client_control); sockset.add(server_control); sockset.add(client_data); sockset.add(server_data); if (sockset.numAdded() == 0) { // all the sockets are INVALID_SOCKET -- there's no connection, // so let's exit the process break; } // add listeners after checking numAdded, because if all we have // are listeners, then the connection is dead sockset.add(client_listen); sockset.add(server_listen); socket_diagnostic( "blocking on select" ", cc=" << (int)client_control << ", sc=" << (int)server_control << ", cd=" << (int)client_data << ", sd=" << (int)server_data << ", cl=" << (int)client_listen << ", sl=" << (int)server_listen); // debugging code that was here, which checked sockets // for exception info, has been moved to end of file // wait until there is activity somewhere sockset.blockUntilReadable(); addEntropy(); // see which socket had activity (ignore case where more than one // did, since the other socket activity will be handled // on a future iteration); once one peer starts to talk, we // ignore the other until the talking peer finishes (there // isn't anything good to do if one starts but never finishes) // --- client sends a request --- if (sockset.contains(client_control)) { socket_diagnostic("select(): activity on client_control"); // NOTE: this code is repeated above active = &client_control; handleClientRequest(); } // --- server sends a reply --- else if (sockset.contains(server_control)) { socket_diagnostic("select(): activity on server_control"); // NOTE: this code is repeated above active = &server_control; handleServerReply(); } // --- ftpd connects to me --- // start of an encrypted non-PASV data transmission else if (sockset.contains(server_listen)) { socket_diagnostic("select(): activity on server_listen"); active = &server_listen; // should not be in PASV mode xassert(!pasvMode()); // connect to client diagnostic("received connection from ftpd, trying to connect to client"); relayDataConnection( server_listen, server_data, relayClientAddress, relayClientPort, client_data, &activeRange); } // --- client connects to me --- // start of an encrypted PASV data transmission else if (sockset.contains(client_listen)) { socket_diagnostic("select(): activity on client_listen"); active = &client_listen; // we should be in PASV mode for this xassert(pasvMode()); // connect to server diagnostic("received connection from client, trying to connect to ftpd"); relayDataConnection( client_listen, client_data, getRemoteAddress(server_control), serverPassivePort, server_data, NULL /*port range is unrestricted*/); } // --- ftpd sends me data --- // activity on the ftpd-side data channel: handling // an encrypted data transmission, server -> client // (PASV and non-PASV) else if (sockset.contains(server_data)) { socket_diagnostic("select(): activity on server_data"); active = &server_data; //diagnostic("received data block from ftpd on local port " << // getLocalPort(server_data)); if (dataChannelProtected()) { encryptDataBlock(); } else { relaySocketData(client_data /*dest*/, server_data /*src*/); } } // --- client sends me data --- // activity on the client-side data channel: handling // an encrypted data transmission, client -> server // (PASV and non-PASV) else if (sockset.contains(client_data)) { socket_diagnostic("select(): activity on client_data"); active = &client_data; //diagnostic("received data block from client on local port " << // getLocalPort(client_data)); if (dataChannelProtected()) { decryptDataBlock(); } else { relaySocketData(server_data /*dest*/, client_data /*src*/); } } // --- select() screws up --- else { // I had been throwing an exception here, but recovery really // is trivial in this case, and all I really need is a report. log(LE_ERROR, "SFTPD INTERNAL BUG: select() call returned but nothing " "was set... unexpected but nonfatal"); } } // --- handle a socket closure (unexpected here) --- catch (xSocket &x) { // this is due to a socket closure log(LE_ERROR, "socket " << sockInfo(x.socket) << " closed: " << x.why()); if (*active == client_control || *active == server_control) { // this should not be possible because handleClientRequest // and handleServerReply should get these... breaker(); // breakpoint if debugger attached closeIf(client_control); closeIf(server_control); break; } if (*active == client_data || *active == server_data) { // close both data channels closeIf(client_data); closeIf(server_data); // we don't expect to detect data channel closures here, // because the data-channel relay code is supposed to // detect and handle them; so this is probably an unexpected // network failure, and the client may want to know (and I // hope this 'unprompted' reply doesn't screw up the // client's internal state machine) unexpected(x); } } catch (xBase &x) { // unknown internal error; tell client and log it unexpected(x); } } // success return; } void SFTPD::handleClientRequest() { try { // here, and below, I'm giving myself the opportunity // to do more graceful shutdown than via an exception // (without the check, we learn of it from StreamLineReader // throwing an exception of a general nature) checkClosed(client_control); // get request diagnostic("got request from client, parsing..."); Request request(*clientControlStream); diagnostic("request from client: " << request.getTextNoPassword()); try { // handle it handleOriginalRequest(request); } catch (xBase &x) { unexpected(x); } } catch (xBase &x) { // client closed connection, do same to server log(LE_CONTROL_CHANNEL, "client control connection closed: " << x.why()); closeSocket(server_control); closeSocket(client_control); } } void SFTPD::handleServerReply() { try { // more-graceful shutdown thing checkClosed(server_control); // get reply diagnostic("ftpd sent a reply, parsing it..."); Reply reply(*serverControlStream); diagnosticNoLF("reply from ftpd: " << reply.getAllText()); try { // handle it handleReply(reply); } catch (xBase &x) { unexpected(x); } } catch (xBase &x) { // server closed connection, do same to client log(LE_CONTROL_CHANNEL, "server control connection closed: " << x.why()); closeSocket(client_control); closeSocket(server_control); } } // called when an unexpected exception is caught void SFTPD::unexpected(xBase &x) { // log it log(LE_ERROR, "UNEXPECTED: " << x.why()); try { // Q: is this a possible security hole? i.e., could it be that // a client could cause an error such that the resulting // exception text would reveal private data (like a key)? // tell client clientReply(RC_INTERNAL_ERROR, x.why()); } catch (xBase &x) { log(LE_ERROR, "while relaying UNEXPECTED: " << x.why()); } } // We've just been contacted by a peer "A", and must establish contact with // the other peer, "B", then accept A's connection. Either A is ftpd and // B is the client, or vice-versa. // peerA_listen - listen()ing socket on which we were just contacted // by peer "A" // peerA_data - socket for accepting A's connection // // addrB, portB - where to connect 'peerB_data' // peerB_data - socket to connect to other peer, "B" // range - if non-NULL, constrain local ports we bind bool SFTPD::relayDataConnection( SOCKET &peerA_listen, SOCKET &peerA_data, IPAddress addrB, int portB, SOCKET &peerB_data, PortRange const *range) { // for errors and diagnostics, we need to determine // who is client and who is server bool AisClient = (peerA_listen == client_listen); char const *A = AisClient? "client" : "server"; char const *B = AisClient? "server" : "client"; // this has been a source of bugs: prevent this fn from // using any of these names (must use peer{A,B}_{listen,data}) #define client_listen No! #define server_listen No! #define client_data No! #define server_data No! try { // try to connect to B first, so if that fails, // we can cause A's conn attempt to fail (this prevents // 0-length files from being created in the case of a // data command that fails this way) // establish the data connection to B diagnostic("trying to connect to " << B << " at " << formatAddress(addrB) << ", port " << portB); peerB_data = connect_socket(addrB, portB); addEntropy(); } catch (xSocket &x) { diagnostic("failed to connect to " << B << "'s data port"); // cause a conn-refused for A closeSocket(peerA_listen); // tell client about the problem if (AisClient) { // ftpd refused the conn.. that's strange unexpected(x); } else { // failed to connect to the client clientReply(RC_FAILED_DATA_CONN_OPEN, stringb("Failed to connect to " << formatAddress(addrB) << ", port " << portB << ": " << x.why())); } return false; } // accept the connection peerA_data = accept_socket(peerA_listen); diagnostic("accepted data connection from " << A); //diagnosticSocketInfo(peerA_data); // close the listener socket; we won't need it anymore closeSocket(peerA_listen); // re-enable use of these names #undef client_listen #undef server_listen #undef client_data #undef server_data return true; } bool SFTPD::controlChannelEncrypted() const { return STATE_AUTHENTICATED <= state && state <= STATE_GOT_PBSZ; } bool SFTPD::dataChannelProtected() const { return dataSecLevel != DSL_CLEAR; } bool SFTPD::pasvMode() const { return serverPassivePort != NONPASV_PORT; } bool SFTPD::amAuthenticating() const { return state == STATE_UNAUTHENTICATED || state == STATE_ADAT; } bool SFTPD::doingDataRelay() const { return dataChannelProtected() || forceDataRelay; } void SFTPD::writeToLog(LogLevel level, LoggableEvent event, char const *msg) { // check masks if (!( (level & logLevelMask) && (event & logEventMask) )) { return; } // decide whether to append a CR or not (LL_NO_LF should be specified // when 'msg' already has a CR; it should *not* be used to try to string // several parameters to 'writeToLog' together on a single output line) char const *fmt = (level & LL_NO_LF)? "%s" : "%s\n"; // only UNIX has syslog, and separately encapsulating this nonportability // doesn't seem to have much value # ifdef __UNIX__ if (useStdinStream && !stdoutLogAnyway) { // log to syslog // (this isn't a very good use of syslog's 'priority' parameter... // somehow I need to rethink my overall logging strategy) syslog(level & LL_DIAGNOSTIC? LOG_DEBUG : LOG_INFO, fmt, msg); return; } # endif // __UNIX__ // log to a file if syslog not avail or we're not running under inetd if (logEventMask & LE_PRINT_TIMESTAMP) { // write timestamp and process id -- since we're not using syslog // we don't get this info for free int month, day, year, hour, minute, second; getCurrentDate(month, day, year); getCurrentTime(hour, minute, second); fprintf(logFile, "[%d] %d/%d/%d %02d:%02d:%02d ", getProcessId(), month, day, year, hour, minute, second); } // log the real message fprintf(logFile, fmt, msg); // flush the damned thing! fflush(logFile); } STATICDEF void SFTPD::warnHandler(WarnLevel level, char const *message) { if (level & WARN_DEBUG) { breaker(); // breakpoint } if (instance) { instance->writeToLog(LL_LOG, LE_WARNING, message); } else { // hmm.. this is certainly not good, but I'm not sure // there's anything good to do about it breaker(); // breakpoint, at least } } void SFTPD::logSocketInfo(LoggableEvent event, SOCKET s) { sockaddr_in local, remote; getSockName(s, local); getSockPeer(s, remote); log(event, " local: " << formatAddress(local) << "\n" << " remote: " << formatAddress(remote)); // no final '\n' because log() adds that } void SFTPD::clientReply(ReplyCode code, char const *msg) { clientReply(Reply(code, msg)); } void SFTPD::clientReply(Reply const &reply) { if (controlChannelEncrypted()) { encryptedClientReply(reply); } else { unencryptedClientReply(reply); } // SHA replies during authentication if (amAuthenticating()) { // this line can be commented-out to deliberately return wrong // digests, to test client's response, during development digt.add(reply); } } void SFTPD::send(Request const &req, SOCKET s) { diagnostic("request to ftpd: " << req.getTextNoPassword()); req.send(s); addEntropy(); } void SFTPD::send(Reply const &reply, SOCKET s) { diagnostic("reply to client: " << reply.getAllText()); // the reply already has a CRLF; this LF adds another // blank line, which tends to separate things nicely reply.send(s); addEntropy(); } // ------------------- request handling ----------------------------- // substitute my port for the client's void SFTPD::portCommandRelay(Request const &request) { // record client port internally if (!getClientPort(request.getData())) { // error reply sent already return; } // bind a socket locally for ftpd to contact eventually listenServerDataPort(); // reply to client clientReply(RC_COMMAND_OK, "PORT command ok."); } // relay a port command unchanged; watch for reflexivity test void SFTPD::portCommandStraightRelay(Request const &request) { // send the request to ftpd send(request, server_control); // get reply Reply reply(*serverControlStream); addEntropy(); diagnostic("(in pCSR) reply from ftpd: " << reply.getAllText()); if (first(reply.getCode()) != RCF_POSITIVE_COMPLETION) { // let's assume this is caused by reflexivity test; switch // to indirect mode, issue the relayed port command; throws // an exception if that fails too diagnostic("switching to indirect mode"); forceDataRelay = true; portCommandRelay(request); } else { // it worked; no big deal diagnostic("port command was accepted"); // 9/22/00 02:01 bugfix: wasn't resetting this serverPassivePort = NONPASV_PORT; // but must send ftpd's reply! this was a bug in 1.10 ... :( clientReply(reply); } } // this fn is called in both 959 interop and normal SafeTP mode void SFTPD::handlePortCommand(Request const &request) { if (failsRequireEncTest()) { return; } if (doingDataRelay()) { // bugfix: clear any prior data-channel socket state closeDataSockets(); // setup our own private listen port, forward that to ftpd, // get its response, and say "ok" to client portCommandRelay(request); } else { // send the port command straight through, but watch for an // error, since we expect certain ftpd's to not like it portCommandStraightRelay(request); } } // the idea is to allow anon connections to be handled entirely // by the normal ftpd, so it can do things like logging and // admission control correctly void SFTPD::possiblyDoAnonExecDropdown(SOCKET whereToSendGreeting) { #ifdef __UNIX__ string userCmd = peekLine(client_control); if (0==missing_stricmp(string(userCmd, 5), "USER ") && isAnonLogin(userCmd + 5)) { // we want to hand things over to the anon ftp // before that, though, we've got to shut down the ftpd we // already started; that's done by closing sockets to it close_socket(server_control); // first, when the anon ftpd gets control, it's going to immediately // send a 220, and we need to protect that from messing up the // client; we do this by sending the 331 the client is expecting, but // NOT sending CRLF, so ftpd's 220 will just be tacked on. // then, ftpd will see the "USER ftp", because we left it in the // buffer // finally, it will respond with a 331. to avoid *that* being seen // as an additional response, we put a dash after the first 331, so // it just looks like a multiline reply. // note that this *will* work even if ftpd sends a multi-line // intro, because RFC 959 specifies that the first line and last // line must have the same reply code (which these do; that's ok), // and that the intermediate lines either start with the same code // or are indented *or* have a dash after the code -- a multi-line // 220 would have intermediate lines that start with 220 but have the // dash sendAllString(whereToSendGreeting, "331-Guest login; handing control to ftpd; its intro: "); // now, since we didn't take the USER command out of the buffer, ftpd // should inherit it // exec the ftpd // "-l" (ell) is wu-ftpd's logging switch execlp(anonFtpdBinary, anonFtpdBinary, "-l", NULL); // only executed if execlp failed xsyserror("execlp"); } // if we didn't see an anon login, drop back into the normal loop // (among other things, this means that the user only has one chance // to trigger this response; but not likely a problem since mostly // people have ftp clients that do it automatically) #else // windows // mainly because we don't have 'exec' under windows.. xfailure("The -e switch is only supported with the Unix SafeTP server"); #endif } // handle a request as it came across the wire void SFTPD::handleOriginalRequest(Request const &request) { if (state == STATE_959_INTEROP) { // forced-relay option if (request.getCmd() == CMD_PORT) { handlePortCommand(request); } else { // just emulating 959; relay to server send(request, server_control); } return; } // we SHA requests during authentication (this will also // SHA a 959 USER or ACCT, but that won't matter) if (amAuthenticating()) { // SHA this request digt.add(request); } switch (request.getCmd()) { case CMD_AUTH: { #if 0 // work in progress if (strchr(request.getData(), '@')) { // special behavior for relay service: // pass it unmodified, and unnoticed send(request, server_control); break; } #endif // 0 if (state != STATE_UNAUTHENTICATED) { // main reason we prevent this is it's unlikely to be // used, and to implement it we'd have to reset ftpd ourselves clientReply(RC_BAD_COMMAND_SEQUENCE, // was parameter-not-implemented, I think bad command sequence is better "Can only AUTH once."); break; } // an AUTH command clears any pre-existing authentication // or authorization (above restriction notwithstanding) if (security) { delete security; } security = NULL; // prevent use of x-cleartext char const *methodName = request.getData(); bool skipQuery = false; if (!allowCleartext && (0==missing_stricmp(methodName, "X-Cleartext") || 0==missing_stricmp(methodName, "X-Cleartext2"))) { skipQuery = true; security = NULL; } // kerberos compatibility if (0==missing_stricmp(methodName, "GSSAPI")) { log(LE_HANDOFF, "client requested GSSAPI"); if (kerberosFtpdBinary.isempty()) { /* Quote from RFC 2228: If the server does not understand the named security mechanism, it should respond with reply code 504. If the server is not willing to accept the named security mechanism, it should respond with reply code 534. */ log(LE_HANDOFF, "but Kerberos compatibility is turned off; see -K switch"); clientReply(RC_REQUEST_DENIED /*534*/, "Kerberos compatibility is turned off. If you'd like to use " "Kerberos with this server, contact the system administrator."); break; } #ifdef __UNIX__ // send an affirmative reply (since that's where in the protocol // kftpd will expect things to be) clientReply(RC_FIRST_ADAT /*334*/, "Ah, you want Kerberos. I'll go get him (it?)... (send security data)"); // NOTE: this won't work unless we are using stdin/stdout as our // sockets. // try to exec the kerberos binary with the special switch hacked // into its sources.. assumes there are no *other* arguments that // need to be passed (what if there are..?) // since the main argument of relevance is -a, which prevents // kftpd from accepting unencrypted connections, let's just supply // it always execl(kerberosFtpdBinary, kerberosFtpdBinary, "-S", "-a", NULL); // if execl returns, it failed string reason = stringb("exec'ing " << kerberosFtpdBinary << " failed: " << strerror(errno)); log(LE_HANDOFF, reason); clientReply(RC_INTERNAL_ERROR, reason); // protocol violation.. who cares #else // windows, etc. clientReply(RC_REQUEST_DENIED, "Kerberos compatibility is only implemented for unix servers. Sorry."); #endif break; } // see if we know about this mechanism if (!skipQuery) { // decide which local address to claim IPAddress localToUse = getMyIPAsClientSeesIt(); // create the security provider security = SecurityProvider:: findSecurityMechanism(methodName, NULL /*i.e., am server*/, localToUse, getRemoteAddress(client_control)); } if (!security) { // unknown security mechanism log(LE_HANDOFF, "client requested unknown security mechanism " << request.getData()); clientReply(RC_PARAMETER_NOT_IMPLEMENTED, stringb("I don't know about mechanism \"" << request.getData() << "\". I know about " << (allowCleartext? "ILST" : "LIST") << "=X-SafeTP1")); // ILST ('incomplete list') if we accept X-Cleartext{2,} break; } // indeed we do.. enter ADAT negotiation composeAdatReply(true /*justAuthed*/); break; } case CMD_ADAT: { if (state != STATE_ADAT) { clientReply(RC_BAD_COMMAND_SEQUENCE, "Must send ADAT only after AUTH."); break; } // ensure 'security' is expecting it xassert(security); xassert(security->control().expectingIncomingAdat()); // un-64 the data DataBlock adat; base64decode(adat, request.getData()); if (printAdats) { adat.print("client adat to server"); } // pass it to 'security' security->control().incomingAdat(adat); // ask 'security' what to do next composeAdatReply(false /*justAuthed*/); break; } case CMD_USER: case CMD_ACCT: { if (state != STATE_UNAUTHENTICATED) { clientReply(RC_PROTECTION_LEVEL_DENIED, "To enter RFC 959 (unencrypted) compatibility mode, " "USER or ACCT must be the *first* command."); break; } char const *loginName = request.getData(); if (checkLoginName(loginName)) { break; // it took action } // this is the *only* place that 'state' might be changed to // STATE_959_INTEROP, and therefore the only place that needs to // check allowRfc959 if (allowRfc959 || (allowRfc959_anon && isAnonLogin(loginName)) || Is959Allowed(loginName) ) { // 959 dropdown state = STATE_959_INTEROP; log(LE_HANDOFF, "dropping down to 959 compatibility mode"); // call this function recursively to handle 959 command handleOriginalRequest(request); } else { // disallow Reply r(RC_PROTECTION_LEVEL_DENIED, "Must send AUTH first. RFC 959 compatibility is off."); r.append("This is a secure FTP server, and you are running an insecure client."); r.append("You can get software to secure your FTP client at:"); r.append("http://safetp.cs.berkeley.edu/"); clientReply(r); } break; } case CMD_QUIT: case CMD_HELP: // could offer info about AUTH/ADAT here... // relay to server, regardless of our state send(request, server_control); break; case CMD_MIC: // supported as if it is ENC case CMD_CONF: // same deal case CMD_ENC: { if (!controlChannelEncrypted()) { clientReply(RC_BAD_COMMAND_SEQUENCE, "Cannot send encrypted requests until authenticated."); break; } xassert(security); try { // allocate and prepare the translation buffer int requestLen = base64length(request.getData()); int bufferLen = security->control().maximumDecodedSize( base64decoder.maxOutputSize(requestLen)) + 1; bufferLen = mymax(bufferLen, requestLen); DataBlock requestBlock((byte const*)request.getData(), requestLen, bufferLen); // un-64 the request base64decoder.trans(requestBlock); // decrypt the request security->control().decode(requestBlock); // add a null terminator requestBlock.addNull(); // debugging output; note that the password protection offered // here is NOT sufficient to conceal the password from the // debug output (because debug output has keys, etc.) Request newReq((char const*)requestBlock.getDataC()); diagnostic("decrypted request: " << newReq.getTextNoPassword()); // handle the request handleDecryptedRequest(newReq); } catch (xSecurity &x) { log(LE_WARNING, "SECURITY VIOLATION: " << x.why()); // go ahead and tell the client.. if a hacker is really // present, this may not get there; but the hacker won't // be able to decrypt it, so there's no harm done (unless // this reveals something critical about the internal // state to a client who is a hacker...) clientReply( Reply(RC_FAILED_SECURITY_CHECK, x.why())); // but, since we may be being hacked, and recovery from things // like sequence # invalid is not generally possible, close // the connection closeConnections(); } break; } case CMD_UNKNOWN: clientReply(RC_UNRECOGNIZED_COMMAND, stringb("I don't understand \"" << request.getCommand() << "\".")); break; default: // firewall friendliness: The issue here is that while many // firewalls (including NAT firewalls) already have provisions // for allowing FTP connections to penetrate, but when we // encrypt the control channel, we hide the protocol commands // necessary for the firewall to know which ports to open. // // So, a solution that works with at least some firewalls // (specifically known to work with Linux IP masquerading and // Windows Internet Connection Sharing) is to send redundant // control traffic in the clear. When the firewall sees this // traffic, it will open the right ports. The same commands // are repeated within in the integrity- and privacy-protected // control channel. // // This traffic is *ignored* by the state machine logic on both // sides -- it is purely for the firewall to act upon. An // attacker who sees the traffic learns nothing useful *if* data // encryption is on; an attacker who modifies this traffic // simply DOSes the connection. #if 1 // set to 0 to disable firewall-friendliness behavior bool supportClientSideFirewallFriendliness = true; if (supportClientSideFirewallFriendliness) { if (request.getCmd() == CMD_PORT) { string portstr = request.getData(); // be careful to strip off any trailing garbage while (portstr.length() && !isdigit(portstr[portstr.length()-1])) { portstr = string(portstr.pchar(), portstr.length()-1); } stringBuilder s("Accepting cleartext PORT command to provide client-side firewall friendliness."); s << " (PORT=" << portstr << ")"; unencryptedClientReply(Reply(RC_COMMAND_OK, s)); break; } else if (request.getCmd() == CMD_LIST) { // we accept LIST too because there might be a firewall that // doesn't open the ports until it sees a real data-transfer // command; since no directory list is actually returned, this // is presumably safe unencryptedClientReply(Reply(RC_FILE_STATUS_OK, "Accepting cleartext LIST command to provide client-side firewall friendliness.")); break; } } #endif clientReply(RC_PROTECTION_LEVEL_DENIED, "I will not accept cleartext commands."); break; } } // the returned value is used in two places: // - as the address supplied in the protocol stream during negotiation; // the client checks this against the address it specified in the // connect() call // - as the address in a reply to PASV; the client will then try to // connect to the address during a data transfer IPAddress SFTPD::getMyIPAsClientSeesIt() const { // decide which local address to claim; normal case (no firewalls, // etc.) is it's just my control channel interface address IPAddress localToUse = getLocalAddress(client_control); // -i: use of an alternate ip address if ( useFakeIPAddress && (getLocalAddress(client_control) != getRemoteAddress(client_control)) ) { // second condition is so localhost isn't faked out // (for testing purposes) localToUse = fakeIPAddress; } return localToUse; } void SFTPD::closeConnections() { closeIf(client_control); closeIf(server_control); closeDataSockets(); } // in addition to being called from closeConnections(), this is called // when we received a PORT or PASV command; it is needed because if // e.g. we got two PASV commands in a row, we want to shut down the // sockets associated with the first before opening new ones (usually // on different ports) for the second void SFTPD::closeDataSockets() { closeIf(client_data); closeIf(server_data); closeIf(client_listen); closeIf(server_listen); } void SFTPD::closeIf(SOCKET &s) { if (s != INVALID_SOCKET) { closeSocket(s); } } void SFTPD::closeSocket(SOCKET &s) { socket_diagnostic("closing socket " << s); close_socket(s); s = INVALID_SOCKET; } // handle a request that was encryped (it has now been decrypted) void SFTPD::handleDecryptedRequest(Request const &request) { xassert(controlChannelEncrypted()); switch (request.getCmd()) { // --------------- 2228 commands ------------------------- case CMD_PBSZ: { if (state != STATE_AUTHENTICATED) { clientReply(RC_BAD_COMMAND_SEQUENCE, "Can send PBSZ only once (for this server), " "after authentication."); break; } // parse the buffer length, represented as decimal integer // as per RFC2228 { unsigned long dataBufferSize = f_ing_strtoul(request.getData(), 10 /*radix*/); if (dataBufferSize < MIN_BUFFER_SIZE) { // 2228 departure; the semantics of RC_PBSZ_TOO_SMALL are that // the client must re-submit the PBSZ request clientReply( RC_PBSZ_TOO_SMALL, stringb("PBSZ is too small; PBSZmin=" << (int)MIN_BUFFER_SIZE)); break; } // store the value, having passed policy tests maxBlockSize = dataBufferSize; } // if it's too big, we just use and send back a smaller number bool wasTooBig = false; if (maxBlockSize > MAX_BUFFER_SIZE) { maxBlockSize = MAX_BUFFER_SIZE; wasTooBig = true; } // compute maximum length of cleartext to fill such a block xassert(security); maxCleartextBlockSize = security-> data().maximumCleartextSizeForBlock(maxBlockSize); // maximum x, such that there is no size s <= x such that // maximumEncodedSize(s) > maxBlockSize // allocate buffer.. we're tacitly assuming that the encoded size // is always at least as large as any of the intermediate forms // (if this turns out to be wrong, then it's a flaw in either our // design, or 2228, or both) dataBuffer.setAllocated(mymax(maxBlockSize, maxCleartextBlockSize)); state = STATE_GOT_PBSZ; if (!wasTooBig) { clientReply(RC_COMMAND_OK, "The PBSZ is ok."); } else { // client must parse this to see the "PBSZ=" clientReply(RC_COMMAND_OK, stringb("PBSZ=" << maxBlockSize)); } break; } case CMD_PROT: { if (state != STATE_GOT_PBSZ) { clientReply(RC_BAD_COMMAND_SEQUENCE, "Can only send PROT after PBSZ."); break; } if (!security) { // not entirely sure what circumstances might provoke this, // but it gets the security==NULL issue out of the following code clientReply(RC_BAD_COMMAND_SEQUENCE, "The current security mechanism does not support PROT."); break; } // map the code into a protection level char protCode = request.getData()[0]; if (protCode != 'C') { DataSecurityLevel level = security->data().getLevelForCode(protCode); if (level == DSL_NONE) { clientReply(RC_PARAMETER_SYNTAX_ERROR, "Unknown PROT command code."); break; // 9/22/00 00:28 bugfix: 'break' was missing } // verify we can support it (should never fail, but now is as good // as time as any to find out if there is a problem) dataSecLevel = level; xassert(dataSecLevel & security->data().getSupportedProtLevels()); } else { // cleartext is implemented directly in sftpd dataSecLevel = DSL_CLEAR; } clientReply(RC_COMMAND_OK, stringb("Data channel protection set to: " << getDSLString(dataSecLevel))); break; } case CMD_AUTH: case CMD_ADAT: clientReply(RC_BAD_COMMAND_SEQUENCE, "Cannot send AUTH nor ADAT after authentication (for this server)."); break; case CMD_CCC: clientReply(RC_REQUEST_DENIED, "CCC not allowed by this server."); break; case CMD_MIC: case CMD_CONF: case CMD_ENC: // this was *already* an encrypted request clientReply(RC_BAD_COMMAND_SEQUENCE, "Cannot nest MIC, CONF, nor ENC."); break; // ----------------- 959 commands --------------------------- case CMD_PORT: handlePortCommand(request); break; // data-channel commands case CMD_RETR: // read from server case CMD_STOR: // write to server case CMD_STOU: // write to server, but don't overwrite existing case CMD_APPE: // append to file on server case CMD_NLST: // names-only directory listing case CMD_LIST: // names + stats directory listing // protocol semantics are that the incrementing is associated // with the *attempt*, so at the earliest moment we see that // the client sent a data-transmission command, we inc if (security && dataChannelProtected()) { if (requireDataEncryption) { // paranioa rule: never call newFile with anything other // than DSL_PRIVATE when requireDataEncryption==true security->data().newFile(DSL_PRIVATE); } else { security->data().newFile(dataSecLevel); } } // policy option; test this *after* newFile to obey file-number // increment semantics if (failsRequireEncTest()) { break; } // If the client hasn't sent a PORT command, that means it is // expecting to receive the connection on its default port, which // is getRemotePort(client_control). But it also means that we // haven't taken the opportunity to tell the server where to contact // sftpd. if (!pasvMode() && doingDataRelay() && server_listen == INVALID_SOCKET) { diagnostic("doing extra PORT for default-client-port transfer"); listenServerDataPort(); } // relay command to server, and assuming it responds with 150 (which // we will relay), expect one of: // (1) server will connect directly to the client, if // data channel protection is off, or // (2) server will open a connection to our listening // local socket (opened in previous PORT command), // in which case we will do data encryption and // relay at that time // (3) client will open a connection to our listening // local socket (opened in previous PASV command), // in which case we also will do encrypt + relay send(request, server_control); break; case CMD_ABOR: // abort data connection - seems to work when encryption // is off and we're out of the way, but not thoroughly // tested goto pass_through; case CMD_PASV: handlePassiveMode(); break; case CMD_USER: if (checkLoginName(request.getData())) { break; // it took action } else { // just fall through to the pass-thru action } // it's inconvenient to track enough state to tell whether // the login succeeded, but it's probably worth it to go // ahead and log the username log(LE_USER, "USER " << request.getData()); // fall-thru pass_through: case CMD_STAT: case CMD_STRU: case CMD_MODE: case CMD_XCUP: case CMD_XCWD: case CMD_XMKD: case CMD_XPWD: case CMD_XRMD: case CMD_SIZE: case CMD_PASS: case CMD_ACCT: case CMD_TYPE: case CMD_CWD: case CMD_CDUP: case CMD_SMNT: case CMD_QUIT: case CMD_ALLO: case CMD_REST: case CMD_RNFR: case CMD_RNTO: case CMD_DELE: case CMD_RMD: case CMD_MKD: case CMD_PWD: case CMD_SITE: case CMD_SYST: case CMD_HELP: case CMD_NOOP: case CMD_MDTM: // pure pass-through to server send(request, server_control); break; // --------------- new commands ------------------------ case CMD_DIGT: { // retrieve the digest DataBlock digtValue = digt.getDigt(); //digtValue.print("sending in response to DIGT"); // base-64 it string b64value = base64encode(digtValue); // reply to client clientReply(RC_SYSTEM_STATUS, stringb("DIGT=" << b64value)); break; } // ----------- unrecognized commands --------------------- default: if (dataChannelProtected()) { // data channel encryption is on, so be cautious clientReply(RC_UNRECOGNIZED_COMMAND, stringb("I don't understand \"" << request.getCommand() << "\".")); } else { // since data channel encryption is off, there is almost certainly // no command that might compromise user's info (beyond what is // already compromised due to no data encryption), so let's just // let the server have a shot at it send(request, server_control); } break; } } // if the transfer isn't allowed, reply to the user and return true; // otherwise return false bool SFTPD::failsRequireEncTest() { if ( requireDataEncryption && !(security && (dataSecLevel == DSL_PRIVATE)) ) { clientReply(RC_REQUEST_DENIED, "This server requires data encryption to be enabled -- turn on " "data encryption in your SafeTP software."); return true; } else { return false; } } // check the login name for special processing; return true if // action was taken, such that the calling code should not do // any more processing bool SFTPD::checkLoginName(char const *name) { if (anonExecDropdown) { // this mode is designed to force anon connections through ftpd // for logging and admission control; we want to prevent users // from circumventing that by giving a bogus initial USER cmd, // and then (after the dropdown check is passed) giving "USER ftp" if (isAnonLogin(name)) { clientReply(RC_REQUEST_DENIED, "If you want an anonymous session, you must say so as " "your *first* command."); return true; } } if (!IsIPAllowed(name)) { clientReply(RC_REQUEST_DENIED, "wrong User name or IP address."); return true; } // didn't do anything return false; } STATICDEF bool SFTPD::isAnonLogin(char const *name) { return 0==strcmp(name, "ftp") || 0==strcmp(name, "anonymous"); } // open a local port for listening for ftpd to connect void SFTPD::listenServerDataPort() { // bugfix: previously I had been opening a listener for all // interfaces, but that could interfere with an independent ftpd on // some other interface IPAddress listenAddr = getLocalAddress(server_control); server_listen = interface_listen_socket(listenAddr, PORT_ANY); int localPort = getLocalPort(server_listen); diagnostic("Listening for ftpd to connect at " << formatAddress(listenAddr, localPort)); // tell ftpd about that socket, blocking while // waiting for ftpd's reply (and throwing an // exception if that reply is not affirmative) Request req(CMD_PORT, constructPortArg( getLocalAddress(server_control), localPort)); serverRequestAndGetReply(req); // now that ftpd has accepted a port command, it's not // in PASV mode (whether it was before, or not) serverPassivePort = NONPASV_PORT; } // send a request to ftpd, get its reply, and verify it is // a success reply; then return the request to the caller Reply SFTPD::serverRequestAndGetReply(Request const &req) { // send request //diagnostic("sending to ftpd: " << req.getText()); send(req, server_control); // wait for non-intermediate reply for (int iters=0; iters < 100; iters++) { bassert(iters < 10); Reply reply(*serverControlStream); addEntropy(); diagnostic("reply from ftpd: " << reply.getAllText()); // get return code ReplyCodeFirst f = first(reply.getCode()); // check for error if (f != RCF_POSITIVE_COMPLETION && f != RCF_POSITIVE_INTERMEDIATE) { // not sure this is the best way to do this... xfailure(stringb("ftpd reply: " << reply.getAllText())); } // success if (f == RCF_POSITIVE_COMPLETION) { return reply; } } xfailure("infinite loop"); // silence warning... return Reply(RC_INTERNAL_ERROR, ""); } // the client has just issued a PASV command; we must relay it to the // server, but instead of passing on the server's response, substitute // our own newly-opened port // // I do not implement the direct-to-ftpd optimization for PASV // transfers.. this originally was simply an oversight, but as I // consider it more, the problem is (unlike PORT), I can't detect // (before it causes a problem) whether the server is going to balk at // the address the client connects from. So my decision now is if // someone really wants the direct-to-ftpd for performance, they can // use active mode. -- sm, 9/22/00 02:07 void SFTPD::handlePassiveMode() { if (failsRequireEncTest()) { return; } // bugfix: clear any prior data-channel socket state closeDataSockets(); // ---------- deal with the server ------------ // pass the request to ftpd, and wait for its reply Reply reply = serverRequestAndGetReply(Request("PASV")); //diagnostic(reply.getAllText()); // parse and return the address and port IPAddress addr; parsePasvArgument(reply.getAllTextAsOneLine(), addr, serverPassivePort); diagnostic("serverPassivePort = " << serverPassivePort); // verify that the server didn't respond with some // random address (this functions as both a debugging // check and as a security check) xassert(addr == getRemoteAddress(server_control)); // verify the server didn't respond with port 0, because // that would confuse my passive-mode detection logic xassert(serverPassivePort != NONPASV_PORT); // ---------- deal with the client ------------ // open a local socket to await the client's connection bassert(client_listen == INVALID_SOCKET); // this is the local address I will bind to make my listen socket IPAddress listenAddr = getLocalAddress(client_control); try { client_listen = interface_listen_socket_range(listenAddr, pasvRange); } catch (xSocket &x) { if (x.temporary) { // couldn't bind any ports clientReply(RC_FILE_ACTION_ERROR /*451*/, x.why()); return; } else { // some other problem throw; } } int localPort = getLocalPort(client_listen); diagnostic("listening for client to connect at " << formatAddress(listenAddr, localPort)); // this is the address I'm going to claim in the PASV reply IPAddress pasvReplyAddr = getMyIPAsClientSeesIt(); // send an affirmative reply to the client clientReply(RC_ENTERING_PASSIVE, stringb("Entering passive mode (" << constructPortArg(pasvReplyAddr, localPort) << ").")); } // parse and store client addr and ip, or reply with failure code // and return false bool SFTPD::getClientPort(char const *values) { // parse client address and port number IPAddress addr; int port; string whyFail; if (!parsePortArgument(values, addr, port, whyFail)) { clientReply(RC_PARAMETER_SYNTAX_ERROR, whyFail); return false; } // check policy about non-reflexive ports if (!allowNonReflexivePorts && addr != getRemoteAddress(client_control)) { clientReply( RC_PARAMETER_SYNTAX_ERROR, // wftpd sends this code in same situation... "I don't allow PORT commands with arbitrary IP addresses."); return false; } // check policy about reserved port numbers if (!allowReservedPorts && port < 1024) { clientReply( RC_PARAMETER_SYNTAX_ERROR, "I don't allow PORT commands with reserved (<1024) port numbers."); return false; } // after passing policy tests, store addr/port relayClientAddress = addr; relayClientPort = port; return true; } // convenience wrapper void SFTPD::encryptedClientReply(Reply const &reply) { // compose the code and the reply text into a single buffer string replyString = reply.getAllText(); diagnosticNoLF("encrypting client reply: " << replyString); // allocate and prepare the translation buffer xassert(security); int replyLen = replyString.length(); int bufferLen = base64encoder.maxOutputSize( security->control().maximumEncodedSize(replyLen)); bufferLen = mymax(replyLen, bufferLen); DataBlock dataBlock((byte const*)replyString.pcharc(), replyLen, bufferLen); // encrypt it security->control().encode(dataBlock); // base-64 it base64encoder.trans(dataBlock); // send it unencryptedClientReply(Reply(RC_PRIVATE_INTEGRITY, (char const*)dataBlock.getDataC())); } // compose and send an ADAT-oriented reply to the client // justAuthed - true if we are replying to the AUTH command, false if // we are replying to the ADAT command void SFTPD::composeAdatReply(bool justAuthed) { // ask 'security' how to proceed string adatString(""); bool outgoing = false; if (security->control().hasOutgoingAdat()) { // the security provider has ADAT(s) to send outgoing = true; DataBlock adat; security->control().getNextOutgoingAdat(adat); if (printAdats) { adat.print("server adat to client"); } // base-64 the data adatString = base64encode(adat); } bool expecting = security->control().expectingIncomingAdat(); // true when the security provider wants the client to // provide some data // determine reply code ReplyCode code; char const *replyText; { // determine code ReplyCodeFirst first = expecting? RCF_POSITIVE_INTERMEDIATE : RCF_POSITIVE_COMPLETION; ReplyCodeSecond second = RCS_AUTHENTICATION; ReplyCodeThird third = justAuthed? RCT_4 : RCT_5; code = makeCode(first, second, third); // determine text (only used when not sending an ADAT reply) static char const * const table[4] = { // justAuthed expecting text // ---------- --------- ---- /* 0 0 */ "Security data exchange complete.", /* 0 1 */ "Received that ADAT; send the next.", /* 1 0 */ "Security mechanism accepted, no ADATs necessary.", /* 1 1 */ "Security mechanism accepted, send first ADAT." }; unsigned index = justAuthed*2 + expecting; xassert(index < TABLESIZE(table)); // paranoia... replyText = table[index]; } // send reply (this will be SHA'd by clientReply) if (outgoing) { clientReply(code, stringb("ADAT=" << adatString)); } else { clientReply(code, replyText); } // adjust state if (!expecting) { state = STATE_AUTHENTICATED; } else { state = STATE_ADAT; } } // send a reply to the client void SFTPD::unencryptedClientReply(Reply const &reply) { send(reply, client_control); } // -------------------- reply handling -------------------------------- void SFTPD::handleReply(Reply const &reply) { // generally, in those cases where we need to do special processing // on the server's reply, we block at the site. as a result, this // general reply handler is only for ordinary situations where we // just relay the server's replies clientReply(reply); } // ------------- data channel handling ------------------------- // read from client, decrypt, forward to server void SFTPD::decryptDataBlock() { // read the block size, 32 bits in network-byte-order; if the // connection has been closed, this will throw xSocket, and // it will be caught and handled in the caller unsigned long blockSize = recvNBO32(client_data); if (blockSize > maxBlockSize) { xsecurity(stringb("Block size was " << blockSize << ", but max is " << maxBlockSize << ".")); } // read the block, blocking until all is received recvAllBlock(client_data, dataBuffer, blockSize); if (printDataChannel) { // note that I am not printing the 4-byte length header, // just because it's somewhat inconvenient to do so dataBuffer.print("encrypted data block received from client"); } // decrypt it xassert(security); security->data().decode(dataBuffer); // this would have been useful debugging the timeout bug diagnostic("sending a decrypted block to ftpd (sizes: plain=" << dataBuffer.getDataLen() << ", enc=" << blockSize << ")"); if (printDataChannel) { dataBuffer.print("sending decrypted data fragment to server"); } // relay it to the server sendAll(server_data, (char const*)dataBuffer.getDataC(), dataBuffer.getDataLen()); addEntropy(); // fixed bug: below, I had been testing blockSize == 0.. this is // clearly wrong because it's the *decrypted* size that matters... // I don't know how such a bug survived this long without being // discovered... // handle EOF if (dataBuffer.getDataLen() == 0) { // since what we just did is decrypt a block of 0 length, that // was the client telling us EOF diagnostic("the client just sent a 0-length block"); // (the file # is not incremented here; rather, we do it in // response to STOR, RETR, etc.) // shut down data sockets closeSocket(server_data); closeSocket(client_data); } doArtificialDelay(); } // read from server, encrypt, forward to client void SFTPD::encryptDataBlock() { // read data from the server unsigned long cleartextBlockSize = recvAllToEOFBlock(server_data, dataBuffer, maxCleartextBlockSize); if (cleartextBlockSize == 0) { diagnostic("ftpd just closed the data connection"); // 9/22/00 01:06 bugfix: shut down the socket now; if something // fails in the code below, we don't want the conn-closed condition // to keep causing us to come here and fail over and over closeSocket(server_data); } if (printDataChannel) { dataBuffer.print("received plaintext data fragment from server"); } // encrypt it xassert(security); security->data().encode(dataBuffer); // verify we aren't exceeding PBSZ unsigned long blockSize = dataBuffer.getDataLen(); xassert(blockSize <= maxBlockSize); // this would have been useful debugging the timeout bug diagnostic("sending an encrypted block to client (sizes: plain=" << cleartextBlockSize << ", enc=" << blockSize << ")"); if (printDataChannel) { dataBuffer.print("sending encrypted data block to client"); } // send the block size sendNBO32(client_data, blockSize); // send the encrypted block to the client sendAll(client_data, (char const*)dataBuffer.getDataC(), blockSize); addEntropy(); // do EOF stuff if (cleartextBlockSize == 0) { // since what we just did is encrypt a block of 0 length, we have // already signalled the client about EOF // (the file # is not incremented here; rather, we do it in // response to STOR, RETR, etc.) // shut down data sockets // (update: moved the close(server_data) up) closeSocket(client_data); } doArtificialDelay(); } void SFTPD::relaySocketData(SOCKET &dest, SOCKET &source) { // paranoia xassert(!requireDataEncryption); // just use a local buffer for simplicity enum { BUFSIZE = 1024 }; char buf[BUFSIZE]; // loop until no more data to relay do { // grab next block of data; don't block, just get // what is already there int len = basicRecv(source, buf, BUFSIZE); if (len == 0) { break; } socket_diagnostic("straight-relaying " << len << " bytes"); // send this data to destination sendAll(dest, buf, len); doArtificialDelay(); // check to see if that was the last of the data } while (pollReadable(source)); // check to see if the source socket has been closed if (isClosed(source)) { closeSocket(source); closeSocket(dest); } } // the current working directory is safe with the daemon, esp. // if the -y switch is used string userConfigDir() { return string("."); } // the purpose of this is to simulate slow links, which is useful for // several reasons: // 1. I had a bug that only showed up when a data transfer took longer // than the server's timeout period, so I want to test my fix without // actually *using* a slow link // 2. I want to test behavior under link-fail conditions, but if the // link is too fast I can't simulate a failure before the transfer // finishes void SFTPD::doArtificialDelay() { if (artificialDelay > 0) { portableSleep(artificialDelay); } } // -------------------- trash --------------------------- #if 0 # if 0 // this was a stab in the dark... // see if any sockets have 'exception' info ready { SocketSet excset; excset.add(client_control); excset.add(server_control); excset.add(client_data); excset.add(server_data); excset.add(client_listen); excset.add(server_listen); timeval zero = {0,0}; if (excset.select(false, false, true, &zero)) { diagnostic("AT LEAST ONE SOCKET HAS EXCEPTIONS WAITING"); // don't know what to do about this, yet } } # endif // 0 // wait for activity on any socket # if 0 diagnostic("blocking on select" ", cc=" << (int)client_control << ", sc=" << (int)server_control << ", cd=" << (int)client_data << ", sd=" << (int)server_data << ", cl=" << (int)client_listen << ", sl=" << (int)server_listen); # endif // 0 // where are we starting? char buf[80]; if (getcwd(buf, 80) == NULL) { syslog(LOG_DEBUG, "getcwd: %m"); exit(2); } // having some difficulties... syslog(LOG_DEBUG, "sftpd is now starting, in directory %s.", buf); #endif // 0 (trash)