/*++ Copyright (c) 1994 Microsoft Corporation Module Name: protocol.cxx Abstract: Contains functions to negotiate data connections with, send commands to and receive data from, the FTP server Contents: Command I_Command NegotiateDataConnection GetReply ReceiveFtpResponse AbortTransfer (SendCommand) (I_SendCommand) (I_AttemptDataNegotiation) (I_NegotiateDataConnection) Author: Heath Hunnicutt (t-hheath) 21-Jun-1994 Environment: Win32 user-level DLL Revision History: --*/ #include #include "ftpapih.h" // // private prototypes // PRIVATE DWORD __cdecl SendCommand( IN ICSocket * s, IN LPCSTR lpszFormat, IN ... ); PRIVATE DWORD I_SendCommand( IN ICSocket * s, IN LPCSTR lpszFormat, va_list arglist ); PRIVATE DWORD I_AttemptDataNegotiation( IN LPFTP_SESSION_INFO lpSessionInfo, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN va_list arglist ); PRIVATE DWORD I_NegotiateDataConnection( IN LPFTP_SESSION_INFO lpSessionInfo, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN va_list arglist, IN BOOL bOverridePassive ); // // functions // DWORD Command( IN LPFTP_SESSION_INFO lpSessionInfo, IN BOOL fExpectResponse, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN ... ) /*++ Routine Description: Sends a command to the FTP server on the control connection and optionally sets up a data connection. Wrapper for I_Command() Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO describing FTP server and our connection to it fExpectResponse - TRUE if we need a data connection dwFlags - controlling how to transfer data between the client and server data connection, and how to set up data connection prcResponse - pointer to response code returned from server lpszCommandFormat - pointer to command string ... - optional arguments for command string Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error --*/ { DEBUG_ENTER((DBG_FTP, Dword, "Command", "%#x, %B, %#x, %#x, %q", lpSessionInfo, fExpectResponse, dwFlags, prcResponse, lpszCommandFormat )); DWORD error; va_list arglist; va_start(arglist, lpszCommandFormat); error = I_Command(lpSessionInfo, fExpectResponse, dwFlags, prcResponse, lpszCommandFormat, arglist ); va_end(arglist); DEBUG_LEAVE(error); return error; } DWORD I_Command( IN LPFTP_SESSION_INFO lpSessionInfo, IN BOOL fExpectResponse, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN va_list arglist ) /*++ Routine Description: Sends a command to the FTP server, and if requested, negotiates a data connection Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO describing the FTP server and our connection to it fExpectResponse - TRUE if we need a data connection dwFlags - controlling how to transfer data between the client and server data connection, and how to set up data connection prcResponse - pointer to returned server response lpszCommandFormat - command to send to server arglist - optional arguments for lpszCommandFormat Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error --*/ { DEBUG_ENTER((DBG_FTP, Dword, "I_Command", "%#x, %B, %#x, %#x, %q, %#x", lpSessionInfo, fExpectResponse, dwFlags, prcResponse, lpszCommandFormat, arglist )); DWORD error; INET_ASSERT(lpSessionInfo != NULL); INET_ASSERT(prcResponse != NULL); INET_ASSERT(lpszCommandFormat != NULL); INET_ASSERT( ((dwFlags & FTP_TRANSFER_TYPE_MASK) == FTP_TRANSFER_TYPE_UNKNOWN) || ((dwFlags & FTP_TRANSFER_TYPE_MASK) == FTP_TRANSFER_TYPE_ASCII) || ((dwFlags & FTP_TRANSFER_TYPE_MASK) == FTP_TRANSFER_TYPE_BINARY) ); // // if the control socket is not valid, return "connection dropped" // if (!lpSessionInfo->socketControl->IsValid()) { error = ERROR_FTP_DROPPED; goto quit; } // // there can only be one data connection established for each FTP session // if (lpSessionInfo->socketData->IsValid()) { error = ERROR_FTP_TRANSFER_IN_PROGRESS; goto quit; } // // if we need a data connection then send the connection set-up commands // and issue the command // if (fExpectResponse) { error = I_AttemptDataNegotiation(lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat, arglist ); if (error == ERROR_SUCCESS) { // // check the server response for failure // if ((prcResponse->Major != FTP_RESPONSE_PRELIMINARY) && (prcResponse->Major != FTP_RESPONSE_COMPLETE)) { error = ERROR_INTERNET_EXTENDED_ERROR; } } } else { // // no data connection required, just send the command and check any // response from the server for a failure indication // error = I_SendCommand(lpSessionInfo->socketControl, lpszCommandFormat, arglist ); if (error == ERROR_SUCCESS) { error = GetReply(lpSessionInfo, prcResponse); if (error == ERROR_SUCCESS) { if ((prcResponse->Major != FTP_RESPONSE_COMPLETE) && (prcResponse->Major != FTP_RESPONSE_CONTINUE) && (prcResponse->Major != FTP_RESPONSE_PRELIMINARY)) { error = ERROR_INTERNET_EXTENDED_ERROR; } } } } quit: DEBUG_LEAVE(error); return error; } DWORD NegotiateDataConnection( IN LPFTP_SESSION_INFO lpSessionInfo, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN ... ) /*++ Routine Description: Sets up a data connection between this client and the FTP server. Wrapper for I_AttemptDataNegotiation() Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO describing the FTP server and our connection to it dwFlags - controlling how to transfer data between the client and server data connection, and how to set up data connection prcResponse - pointer to returned server response code lpszCommandFormat - command string to send ... - optional arguments for lpszCommandFormat Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error error returned by Windows sockets --*/ { DEBUG_ENTER((DBG_FTP, Dword, "NegotiateDataConnection", "%#x, %#x, %#x, %q", lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat )); va_list arglist; va_start(arglist, lpszCommandFormat); DWORD error = I_AttemptDataNegotiation(lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat, arglist ); va_end(arglist); DEBUG_LEAVE(error); return error; } DWORD GetReply( IN LPFTP_SESSION_INFO lpSessionInfo, OUT FTP_RESPONSE_CODE *prcResponse ) /*++ Routine Description: Gets the response code from the server. The response text is stored in the per-thread last response text field, and the FTP response code is parsed off the start of the text and returned in prcResponse Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO for which to get response text prcResponse - pointer to the response code structure Return Value: DWORD Success - ERROR_SUCCESS Failure - Win32 error --*/ { DEBUG_ENTER((DBG_FTP, Dword, "GetReply", "%#x, %#x", lpSessionInfo, prcResponse )); PCHAR pchReplyBuffer; DWORD cchBufferLength; DWORD error; INET_ASSERT(prcResponse != NULL); // // set up a default error code // prcResponse->Major = FTP_RESPONSE_PERMANENT_FAILURE; prcResponse->Minor = 0; prcResponse->Detail = 0; prcResponse->Status = 0; pchReplyBuffer = NULL; // // receive the last response on the control socket // error = ReceiveFtpResponse(lpSessionInfo->socketControl, (LPVOID *)&pchReplyBuffer, &cchBufferLength, TRUE, prcResponse ); if (error == ERROR_SUCCESS) { INET_ASSERT(pchReplyBuffer != NULL); // // append this response text to that already stored in this thread's // data object // InternetSetLastError(0, pchReplyBuffer, cchBufferLength, SLE_APPEND | SLE_ZERO_TERMINATE ); // // extract status code // LPSTR lpszError = pchReplyBuffer; ExtractInt(&lpszError, 0, &prcResponse->Status); DEBUG_PRINT(PROTOCOL, INFO, ("FTP status = %d\n", prcResponse->Status)); } else { INET_ASSERT(pchReplyBuffer == NULL); } // // finished with the buffer // if (pchReplyBuffer != NULL) { pchReplyBuffer = (PCHAR)FREE_MEMORY((HLOCAL)pchReplyBuffer); INET_ASSERT(pchReplyBuffer == NULL); } DEBUG_LEAVE(error); return error; } DWORD ReceiveFtpResponse( IN ICSocket * Socket, OUT LPVOID * lpBuffer, OUT LPDWORD lpdwBufferLength, IN BOOL fEndOfLineCheck, IN FTP_RESPONSE_CODE * prcResponse ) /*++ Routine Description: Receives data from the FTP server. Optionally parses it for end-of-line sequence This function is typically going to receive one or more lines of response data, i.e. a small amount of data. Also typically, we will not normally expect to get EOF(connection) because we are usually reading the control connection Arguments: Socket - socket on which to receive data lpBuffer - pointer to pointer to returned data buffer lpdwBufferLength - pointer to returned length of (returned) data buffer fEndOfLineCheck - TRUE if we are to perform an end-of-line check prcResponse - pointer to returned server response code Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error --*/ { DEBUG_ENTER((DBG_FTP, Dword, "ReceiveFtpResponse", "%#x, %#x, %#x, %B, %#x", Socket, lpBuffer, lpdwBufferLength, fEndOfLineCheck, prcResponse )); INET_ASSERT(lpBuffer != NULL); INET_ASSERT(lpdwBufferLength != NULL); if (fEndOfLineCheck) { INET_ASSERT(prcResponse != NULL); } LPSTR pchBuffer; DWORD bufferLength; DWORD bufferLeft; DWORD bytesReceived; int idxPosInLine; BOOL fLastLineDigitsSeen; BOOL fAtEOL; int nNumericReply; DWORD error; // // initialize variables (for SocketReceive()) // pchBuffer = NULL; bufferLength = 0; bufferLeft = 0; bytesReceived = 0; // // get per-thread info block to determine blocking/non-blocking socket calls // LPINTERNET_THREAD_INFO lpThreadInfo = InternetGetThreadInfo(); if (lpThreadInfo == NULL) { INET_ASSERT(FALSE); error = ERROR_INTERNET_INTERNAL_ERROR; goto done; } //DWORD asyncFlags; // //asyncFlags = 0; //asyncFlags = lpThreadInfo->IsAsyncWorkerThread ? SF_NON_BLOCKING : 0; // // this flag is set to FALSE once the digits are definitely NOT seen, and // needs to be TRUE at the start of each new line // fLastLineDigitsSeen = TRUE; nNumericReply = 0; idxPosInLine = 0; fAtEOL = FALSE; BOOL eofData; while (TRUE) { // // get the next chunk of response data from the server. Since we are // expecting a response (i.e. small amount of data (< 1K)) we shouldn't // have to do this too many times // error = Socket->Receive( (LPVOID *)&pchBuffer, &bufferLength, &bufferLeft, &bytesReceived, sizeof('\0'), // dwExtraSpace SF_EXPAND // SocketReceive will allocate/grow the buffer | SF_COMPRESS // and compress any unused space at EOF | SF_INDICATE, // and make indications to app &eofData ); if (error != ERROR_SUCCESS) { goto quit; } // // if we have hit the end of the data then zero-terminate the buffer // if (eofData) { INET_ASSERT(bufferLeft > 0); pchBuffer[bytesReceived] = '\0'; } // // if requested, find out if we got the end-of-line (on the last line) // if (fEndOfLineCheck) { // // look for the final line of the response // for (DWORD bytesChecked = 0; bytesChecked < bytesReceived; ++bytesChecked, ++idxPosInLine) { // // ISSUE: fAtEOL && fLastLineDigitsSeen ==> protocol error, // since there is data after the response line // // // if the previous character was an EOL, then the next one must // be the start of the next line. Reinitialize the response- // decoding state // if (fAtEOL) { fLastLineDigitsSeen = TRUE; nNumericReply = 0; idxPosInLine = 0; fAtEOL = FALSE; } // // if (so far) this line has been of the form we expect for a // response last-line, check the chars we just received to see // if it maintains that form // // since this form is completely determined by the first four // characters of the line, limit the checking to those chars // if (fLastLineDigitsSeen && (idxPosInLine < 4)) { if (idxPosInLine < 3) { // // if one of the first three chars is not a digit, mark // this line as a non-response // if (!isdigit(pchBuffer[bytesChecked])) { fLastLineDigitsSeen = FALSE; } else { // // store the numeric reply in an int // nNumericReply *= 10; nNumericReply += (int)(pchBuffer[bytesChecked] - '0'); } } else if (idxPosInLine == 3) { // // the first three chars must be digits. If the fourth is a // space, then this line has the right form // if (pchBuffer[bytesChecked] != ' ') { fLastLineDigitsSeen = FALSE; } } } // // if at the end of a line, reset vars for the next line // if (pchBuffer[bytesChecked] == '\n') { DEBUG_PRINT(PROTOCOL, INFO, ("At EOL, and fDigits = %B\n", fLastLineDigitsSeen )); fAtEOL = TRUE; } } } // // if this data is expected to end in a proper response line, and that // line has been received, stop receiving more data // if (fEndOfLineCheck && fLastLineDigitsSeen && fAtEOL) { // // we found the end of the response, although we may not have // received an EOF(transmission) indication, because the server // didn't close the connection. Zero terminate the buffer // INET_ASSERT(bufferLeft > 0); pchBuffer[bytesReceived] = '\0'; // // tear the numeric reply up by digit, placing it into our nicer // reply structure // INET_ASSERT(nNumericReply >= 0); INET_ASSERT(nNumericReply < 1000); prcResponse->Major = (nNumericReply / 100) % 10; prcResponse->Minor = (nNumericReply / 10) % 10; prcResponse->Detail = (nNumericReply) % 10; error = ERROR_SUCCESS; goto done; } // // if we were receiving data that was supposed to end in an EOL then this // is an error condition. Otherwise, it is the expected way to signal the // end of transmission // if (eofData) { if (fEndOfLineCheck) { error = ERROR_FTP_DROPPED; goto quit; } else { // // caller just receiving data. We're done // error = ERROR_SUCCESS; goto done; } } } INET_ASSERT(FALSE); quit: INET_ASSERT(error != ERROR_SUCCESS); if (pchBuffer != NULL) { pchBuffer = (LPSTR)FREE_MEMORY((HLOCAL)pchBuffer); INET_ASSERT(pchBuffer == NULL); } done: *lpBuffer = pchBuffer; *lpdwBufferLength = bytesReceived; INET_ASSERT((pchBuffer != NULL) ? (*(LPDWORD)pchBuffer != 0xc5c5c5c5) : TRUE); DEBUG_LEAVE(error); return error; } DWORD AbortTransfer( IN LPFTP_SESSION_INFO lpSessionInfo ) /*++ Routine Description: Aborts an ongoing transfer. Typically used to terminate a read file operation early. We don't expect a response from the server in this case Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO containing context for sesion to abort Return Value: DWORD Success - ERROR_SUCCESS Failure - ERROR_INTERNET_EXTENDED_ERROR --*/ { DEBUG_ENTER((DBG_FTP, Dword, "AbortTransfer", "%#x", lpSessionInfo )); // // get per-thread info block to determine blocking/non-blocking socket calls // LPINTERNET_THREAD_INFO lpThreadInfo = InternetGetThreadInfo(); DWORD error; if (lpThreadInfo == NULL) { INET_ASSERT(FALSE); error = ERROR_INTERNET_INTERNAL_ERROR; goto quit; } //DWORD asyncFlags; // //asyncFlags = 0; //asyncFlags = lpThreadInfo->IsAsyncWorkerThread ? SF_NON_BLOCKING : 0; // // send an ABORT preceded by the following NVT/Telnet sequences: // // FF F4 = Interrupt Process (IP) // FF F2 = Data Mark (DM) // #define ABORT_COMMAND NVT_INTERRUPT_PROCESS_STRING \ NVT_DATA_MARK_STRING \ "ABOR" \ "\r\n" // // BUGBUG - the IP/DM sequence should be sent as URGENT data // error = lpSessionInfo->socketControl->Send((LPBYTE)ABORT_COMMAND, sizeof(ABORT_COMMAND) - 1, SF_INDICATE ); quit: DEBUG_LEAVE(error); return error; } // // private functions // PRIVATE DWORD __cdecl SendCommand( IN ICSocket * Socket, IN LPCSTR lpszFormat, IN ... ) /*++ Routine Description: Sends a command string to the FTP server. Wrapper for I_SendCommand() Arguments: Socket - socket to send data on lpszFormat - printf-style format string ... - arguments for lpszFormat Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error --*/ { DEBUG_ENTER((DBG_FTP, Dword, "SendCommand", "%#x, %q", Socket, lpszFormat )); va_list arglist; DWORD error; va_start(arglist, lpszFormat); error = I_SendCommand(Socket, lpszFormat, arglist ); va_end(arglist); DEBUG_LEAVE(error); return error; } PRIVATE DWORD I_SendCommand( IN ICSocket * Socket, IN LPCSTR lpszFormat, va_list arglist ) /*++ Routine Description: Sends a command string to a server Arguments: Socket - socket to send command on lpszFormat - printf-style format string for command arglist - variable list of arguments for format string Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error error returned from Windows Sockets ERROR_INTERNET_INTERNAL_ERROR The string was too large to fit into our stack buffer --*/ { DEBUG_ENTER((DBG_FTP, Dword, "I_SendCommand", "%#x, %q, %#x", Socket, lpszFormat, arglist )); #define I_SEND_COMMAND_BUFFER_LENGTH 2048 // Arbitrary (but large) // // get per-thread info block to determine blocking/non-blocking socket calls // LPINTERNET_THREAD_INFO lpThreadInfo = InternetGetThreadInfo(); DWORD error; if (lpThreadInfo == NULL) { INET_ASSERT(FALSE); error = ERROR_INTERNET_INTERNAL_ERROR; goto quit; } // // we need a buffer large enough to be able to handle the longest path plus // the command overhead. Typically, we will only be getting short strings // (USER, PORT, etc.) // // // BUGBUG // use _vsnprintf() to create the buffer - this allows us to specify the // maximum offset in the buffer, so avoiding a stack crash but this // brings in the cruntimes which we don't want to do. // CHAR buf[I_SEND_COMMAND_BUFFER_LENGTH]; { int numChars = wvnsprintf(buf, I_SEND_COMMAND_BUFFER_LENGTH, lpszFormat, arglist); INET_ASSERT(numChars <= I_SEND_COMMAND_BUFFER_LENGTH - 2); INET_ASSERT(numChars > 0); if (numChars <= I_SEND_COMMAND_BUFFER_LENGTH - 2) { // // append trailing "\r\n" // buf[numChars++] = '\r'; buf[numChars++] = '\n'; error = Socket->Send((LPVOID)buf, numChars, SF_INDICATE); } else { DEBUG_PRINT(PROTOCOL, ERROR, ("%d chars blows internal buffer limit (%d chars)\n", numChars, I_SEND_COMMAND_BUFFER_LENGTH )); error = ERROR_INVALID_PARAMETER; } } quit: DEBUG_LEAVE(error); return error; } PRIVATE DWORD I_AttemptDataNegotiation( IN LPFTP_SESSION_INFO lpSessionInfo, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN va_list arglist ) /*++ Routine Description: Attempts to generate a data connection with server. If passive mode is selected & passive mode fails, then we back-down to non-passive Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO describing server to connect to dwFlags - controlling how to transfer data between the client and server data connection, and how to set up data connection prcResponse - pointer to response code variable lpszCommandFormat - command to send arglist - any arguments for command Return Value: DWORD Success - ERROR_SUCCESS Failure - same as I_NegotiateDataConnection --*/ { DEBUG_ENTER((DBG_FTP, Dword, "I_AttemptDataNegotiation", "%#x, %#x, %#x, %q, %#x", lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat, arglist )); // // first try it without overriding passive mode // DWORD error = I_NegotiateDataConnection(lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat, arglist, FALSE ); // // if the negotiation failed because passive mode wasn't supported or not // allowed then try non-passive (if ok to do so) // if (error == ERROR_FTP_NO_PASSIVE_MODE) { INET_ASSERT(IsPassiveModeSession(lpSessionInfo)); error = I_NegotiateDataConnection(lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat, arglist, TRUE // no passive mode this time ); } DEBUG_LEAVE(error); return error; } PRIVATE DWORD I_NegotiateDataConnection( IN LPFTP_SESSION_INFO lpSessionInfo, IN DWORD dwFlags, IN OUT FTP_RESPONSE_CODE *prcResponse, IN LPCSTR lpszCommandFormat, IN va_list arglist, IN BOOL bOverridePassive ) /*++ Routine Description: Sends a command to the FTP server and opens a data channel Arguments: lpSessionInfo - pointer to FTP_SESSION_INFO describing server to connect to dwFlags - controlling how to transfer data between the client and server data connection, and how to set up data connection prcResponse - pointer to response code variable lpszCommandFormat - command to send arglist - any arguments for command bOverridePassive - TRUE if OK to override passive mode (set at connect level) Return Value: DWORD Success - ERROR_SUCCESS Failure - WSA error --*/ { DEBUG_ENTER((DBG_FTP, Dword, "I_NegotiateDataConnection", "%#x, %#x, %#x, %q, %#x, %B", lpSessionInfo, dwFlags, prcResponse, lpszCommandFormat, arglist, bOverridePassive )); PUCHAR puchAddr; PCHAR pch; ICSocket * socketControl; ICSocket * socketData ; ICSocket * socketListener ; DWORD error; int serr; LPINTERNET_THREAD_INFO lpThreadInfo = InternetGetThreadInfo(); BOOL isAsync; BOOL bPassiveMode = FALSE; if (lpThreadInfo == NULL) { INET_ASSERT(FALSE); error = ERROR_INTERNET_INTERNAL_ERROR; goto quit; } isAsync = lpThreadInfo->IsAsyncWorkerThread; //Command(lpSessionInfo, // FALSE, // FTP_TRANSFER_TYPE_UNKNOWN, // prcResponse, // "MODE B" //// "MODE S" // ); // // tell the server the type of transfer we want - ASCII ('A') or Binary ('I') // error = Command(lpSessionInfo, FALSE, FTP_TRANSFER_TYPE_UNKNOWN, prcResponse, "TYPE %s", ((dwFlags & FTP_TRANSFER_TYPE_MASK) == FTP_TRANSFER_TYPE_ASCII) ? "A" : "I" ); if (error != ERROR_SUCCESS) { goto quit; } if (prcResponse->Major != FTP_RESPONSE_COMPLETE) { error = ERROR_INTERNET_EXTENDED_ERROR; goto quit; } socketControl = lpSessionInfo->socketControl; socketData = lpSessionInfo->socketData; socketListener = lpSessionInfo->socketListener; INET_ASSERT(!socketData->IsValid()); INET_ASSERT(!socketListener->IsValid()); // // Get the address of our control socket. We need to know what address // family (IPv4 or IPv6) to use for our data connection. And in the // active case, we need to provide this address to the server. // SOCKADDR_STORAGE ourCtrlAddr; error = socketControl->GetSockName((LPSOCKADDR)&ourCtrlAddr, sizeof(ourCtrlAddr)); if (error != ERROR_SUCCESS) { goto error_exit; } // // we make passive connection if passive mode was specified in // InternetConnect() AND we are not overriding it // bPassiveMode = IsPassiveModeSession(lpSessionInfo) && !bOverridePassive; // // make the required type of data connection // if (!bPassiveMode) { SOCKADDR_STORAGE ourDataAddr; int cbAddrLen; // // standard (non-passive (ACTIVE?)) transfers. We create a socket and // tell the server which socket to connect to. The server then initiates // the connection with this client // error = socketListener->CreateSocket(0, ourCtrlAddr.ss_family, SOCK_STREAM, IPPROTO_TCP); if (error != ERROR_SUCCESS) { goto error_exit; } #if INET_DEBUG socketListener->SetSourcePort(); #endif error = socketListener->Listen(); if (error != ERROR_SUCCESS) { goto error_exit; } // // Now tell the server what address and port number it should use to // connect to us. The address is the local side of our control // connection, and the port number is that of the listening socket. // error = socketListener->GetSockName((LPSOCKADDR)&ourDataAddr, sizeof(ourDataAddr)); if (error != ERROR_SUCCESS) { goto error_exit; } // // BUGBUG - using (global) winsock ntohs() (i.e. not SOCKS) // u_short port; if (ourCtrlAddr.ss_family == AF_INET) { // // Legacy IPv4. The PORT command consists of our IP address // formatted as 4 decimal numbers (one per byte) followed by // the port number as 2 decimals numbers (one per byte). // Byte order is big-endian. // // REVIEW: RFC 2428 describes the EPRT command, which is // intended to replace the PORT command. Should we try it // first? At the moment, we only attempt EPRT over IPv6. // port = _I_ntohs(((LPSOCKADDR_IN)&ourDataAddr)->sin_port); puchAddr = (PUCHAR)&((LPSOCKADDR_IN)&ourCtrlAddr)->sin_addr; error = SendCommand(socketControl, "PORT %d,%d,%d,%d,%d,%d", puchAddr[0], puchAddr[1], puchAddr[2], puchAddr[3], HIBYTE(port), LOBYTE(port) ); } else { // // IPv6. The EPRT command is defined in RFC 2428, and has the // following format: // EPRT
// where is a delimiter character ('|' recommended), // is an IANA defined number (1=IPv4, 2=IPv6), //
is the IP address in common string notation, // is the port number in common string notation. // #define MAX_IPV6_ADDR_LIT_LEN (sizeof("1111:2222:3333:4444:5555:6666:255.255.255.255%4294967295")) char Address[MAX_IPV6_ADDR_LIT_LEN]; error = _I_getnameinfo((LPSOCKADDR)&ourCtrlAddr, sizeof(SOCKADDR_IN6), Address, sizeof(Address), NULL, 0, NI_NUMERICHOST); if (error) { goto error_exit; } port = _I_ntohs(((LPSOCKADDR_IN6)&ourDataAddr)->sin6_port); error = SendCommand(socketControl, "EPRT |2|%s|%u", Address, port); } if (error != ERROR_SUCCESS) { goto error_exit; } // // read the response from the server. We are expecting something along // the lines: "200 PORT Command Successful" // error = GetReply(lpSessionInfo, prcResponse); if (error != ERROR_SUCCESS) { goto error_exit; } if (prcResponse->Major != FTP_RESPONSE_COMPLETE) { // // Some IPv6 servers use LPRT instead of EPRT. So we try that. // REVIEW: If we change the IPv4 case above to try EPRT first // (see REVIEW comment above) then we'd try PORT here for IPv4. // if ((ourCtrlAddr.ss_family == AF_INET6) && (prcResponse->Status == FTP_RESPONSE_CMD_NOT_IMPL) || (prcResponse->Status == FTP_RESPONSE_CMD_SYNTAX_ERROR)) { error = SendCommand(socketControl, "LPRT 6,16,%u,%u,%u,%u," "%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u," "2,%u,%u", ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[0], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[1], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[2], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[3], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[4], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[5], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[6], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[7], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[8], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[9], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[10], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[11], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[12], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[13], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[14], ((LPSOCKADDR_IN6)(&ourCtrlAddr))->sin6_addr.s6_addr[15], HIBYTE(port), LOBYTE(port) ); if (error != ERROR_SUCCESS) { goto error_exit; } error = GetReply(lpSessionInfo, prcResponse); if (error != ERROR_SUCCESS) { goto error_exit; } if (prcResponse->Major == FTP_RESPONSE_COMPLETE) goto GotActiveReply; } error = ERROR_INTERNET_EXTENDED_ERROR; goto error_exit; } } else { PCHAR pchTemp; // // PASSIVE mode. Due to problems with firewalls not allowing incoming // connection requests, we have to ask the server to create a new socket // which we then connect to. This is the inverse of the non-PASV connect // case above // error = socketData->CreateSocket(0, ourCtrlAddr.ss_family, SOCK_STREAM, IPPROTO_TCP); if (error != ERROR_SUCCESS) { goto error_exit; } // // send the PASV request to the server // if (ourCtrlAddr.ss_family == AF_INET) { // REVIEW: Attempt EPSV first for IPv4 as well? error = SendCommand(socketControl, "PASV"); } else { error = SendCommand(socketControl, "EPSV"); } if (error != ERROR_SUCCESS) { goto error_exit; } // // Read the response. For the "PASV" command, we are expecting it // to return the full address information for the port in the form // "227 (h1,h2,h3,h4,p1,p2)" // meaning that the server is entering PASSIVE mode (227), and the // IP address and socket to connect to being h1.h2.h3.h4, p1p2. // // For the "EPSV" command, we are expecting a response of the form // "229 ()" // meaning that the server is entering PASSIVE mode (229), the // IP address to connect to is that of the control connection, and // the port number to connect to is given by . // error = GetReply(lpSessionInfo, prcResponse); if (error != ERROR_SUCCESS) { goto error_exit; } if (prcResponse->Major != FTP_RESPONSE_COMPLETE) { // // Some IPv6 servers use LPSV instead of EPSV. // The wonderful thing about standards is that // there are so many to choose from. // if ((ourCtrlAddr.ss_family == AF_INET6) && ((prcResponse->Status == FTP_RESPONSE_CMD_NOT_IMPL) || (prcResponse->Status == FTP_RESPONSE_CMD_SYNTAX_ERROR))) { error = SendCommand(socketControl, "LPSV"); if (error != ERROR_SUCCESS) { goto error_exit; } error = GetReply(lpSessionInfo, prcResponse); if (error != ERROR_SUCCESS) { goto error_exit; } if (prcResponse->Major == FTP_RESPONSE_COMPLETE) goto GotPassiveReply; } // // If we get 500 or 502 (command not implemented), 425 // (can't open data connection), or 530 (not logged in) // then return ERROR_FTP_NO_PASSIVE_MODE, else // ERROR_INTERNET_EXTENDED_ERROR. // if ((prcResponse->Status == FTP_RESPONSE_CANT_OPEN_DATA) || (prcResponse->Status == FTP_RESPONSE_CMD_NOT_IMPL) || (prcResponse->Status == FTP_RESPONSE_CMD_SYNTAX_ERROR) || (prcResponse->Status == FTP_RESPONSE_NOT_LOGGED_IN)) { error = ERROR_FTP_NO_PASSIVE_MODE; } else { error = ERROR_INTERNET_EXTENDED_ERROR; } goto error_exit; } GotPassiveReply: // // parse the endpoint out of the server response (stored in the per- // thread last response info buffer). // // If we fail to lock the response text, then that's an internal error. // If we fail to parse the required information out of the response text // then we return an extended error indication, since presumably the // server sent response text, but not what we were expecting // pch = (PCHAR)InternetLockErrorText(); if (pch != NULL) { if (ourCtrlAddr.ss_family == AF_INET) { // // Parse respone to PASV command. // pch = strstr(pch, "227 "); if (pch != NULL) { pch += sizeof("227 ") - 1; while (!isdigit(*pch) && *pch) { ++pch; } if (isdigit(*pch)) { int i; DWORD octets[6]; // // parse the individual address parts out of the response // for (i = 0; (i < ARRAY_ELEMENTS(octets)) && isdigit(*pch); ++i) { if (!ExtractInt(&pch, 0, (LPINT)&octets[i])) { break; } if (octets[i] > 255) { break; } if (i < ARRAY_ELEMENTS(octets) - 1) { while (!isdigit(*pch) && *pch) { ++pch; } } } // // if we successfully parsed the server's address info then // try to connect with the address it sent us // if ((i == ARRAY_ELEMENTS(octets)) && (octets[i - 1] <= 255)) { SOCKADDR_IN remoteSockaddr; memset(&remoteSockaddr, 0, sizeof(remoteSockaddr)); remoteSockaddr.sin_family = AF_INET; // // N.B. Using (global) winsock ntohs() (i.e. not SOCKS) // remoteSockaddr.sin_port = _I_htons( (USHORT)((octets[4] << 8) | (USHORT)octets[5])); puchAddr = (PUCHAR)&remoteSockaddr.sin_addr; puchAddr[0] = (UCHAR)octets[0]; puchAddr[1] = (UCHAR)octets[1]; puchAddr[2] = (UCHAR)octets[2]; puchAddr[3] = (UCHAR)octets[3]; error = socketData->DirectConnect( (LPSOCKADDR)&remoteSockaddr); } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to parse 6 address elements\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to locate start of address info in response text\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to locate \"227\" in response text\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else { CHAR delim; // // Parse respone to EPSV or LPSV command. // pchTemp = strstr(pch, "229 "); if (pchTemp != NULL) { pch = pchTemp + sizeof("229 ") - 1; while ((*pch != '(') && (*pch != '\0')) { ++pch; } if ((pch[0] == '(') && ((delim = pch[1]) != '\0') && (pch[2] == delim) && (pch[3] == delim)) { DWORD port; // // parse the port out of the response // pch += 4; if (ExtractInt(&pch, 0, (LPINT)&port)) { // // if we successfully parsed the port then // try to connect with it // SOCKADDR_IN6 remoteSockaddr; error = socketControl->GetPeerName( (LPSOCKADDR)&remoteSockaddr, sizeof(SOCKADDR_IN6)); if (error == ERROR_SUCCESS) { // // N.B. Using (global) winsock ntohs() // (i.e. not SOCKS) // remoteSockaddr.sin6_port = _I_htons((USHORT)port); error = socketData->DirectConnect( (LPSOCKADDR)&remoteSockaddr); } } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to parse port\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to locate start of address info in response text\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else if ((pchTemp = strstr(pch, "228 ")) != NULL) { pch = pchTemp + sizeof("228 ") - 1; while (!isdigit(*pch) && *pch) { ++pch; } if (isdigit(*pch)) { int i; DWORD octets[21]; // // Parse the individual address parts out of // the response. // for (i = 0; (i < ARRAY_ELEMENTS(octets)) && isdigit(*pch); ++i) { if (!ExtractInt(&pch, 0, (LPINT)&octets[i])) { break; } if (octets[i] > 255) { break; } if (i < ARRAY_ELEMENTS(octets) - 1) { while (!isdigit(*pch) && *pch) { ++pch; } } } // // If we successfully parsed the server's address info // then try to connect with the address it sent us. // if ((i == ARRAY_ELEMENTS(octets)) && (octets[i - 1] <= 255) && (octets[0] == 6) && (octets[1] == 16) && (octets[18] == 2)) { SOCKADDR_IN6 remoteSockaddr; memset(&remoteSockaddr, 0, sizeof remoteSockaddr); remoteSockaddr.sin6_family = AF_INET6; // // N.B. Using (global) winsock ntohs() // (i.e. not SOCKS) // remoteSockaddr.sin6_port = _I_htons( (USHORT)((octets[19] << 8) | (USHORT)octets[20])); for (i = 0; i < 16; i++) remoteSockaddr.sin6_addr.s6_addr[i] = (UCHAR)octets[i + 2]; error = socketData->DirectConnect( (LPSOCKADDR)&remoteSockaddr); } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to parse 20 address elements\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to locate start of address info in response text\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to locate \"229\" or \"228\" in response text\n" )); error = ERROR_INTERNET_EXTENDED_ERROR; } } // // unlock the response text // //InternetUnlockErrorText(); } else { DEBUG_PRINT(PROTOCOL, ERROR, ("failed to lock last response text\n" )); error = ERROR_INTERNET_INTERNAL_ERROR; } // // bail out if we met with any errors // if (error != ERROR_SUCCESS) { goto error_exit; } } GotActiveReply: // // issue SIZE command to get file size // if(StrCmpNI(lpszCommandFormat, "RETR", 4) == 0) { // If we're downloading a file error = I_SendCommand(socketControl, "SIZE %s", arglist ); if (error != ERROR_SUCCESS) { goto error_exit; } error = GetReply(lpSessionInfo, prcResponse); if (error != ERROR_SUCCESS) { if (socketData->IsValid()) { ResetSocket(socketData); } goto error_exit; } if (prcResponse->Major == FTP_RESPONSE_COMPLETE) { pch = (PCHAR)InternetLockErrorText(); if (pch != NULL) { pch = strstr(pch, "213 "); if (pch != NULL) { pch += sizeof("213 ") - 1; if ( pch ) { if (*pch && isdigit(*pch)) { if (ExtractInt(&pch, 0, (int *) &(lpSessionInfo->dwFileSizeLow))) { lpSessionInfo->Flags |= FFTP_KNOWN_FILE_SIZE; } } } } } } } // // we have the data connection, send the command // error = I_SendCommand(socketControl, lpszCommandFormat, arglist ); if (error != ERROR_SUCCESS) { goto error_exit; } // // get the preliminary reply from the FTP server // error = GetReply(lpSessionInfo, prcResponse); if (error != ERROR_SUCCESS) { if (socketData->IsValid()) { ResetSocket(socketData); } goto error_exit; } if ((prcResponse->Major != FTP_RESPONSE_PRELIMINARY) && (prcResponse->Major != FTP_RESPONSE_COMPLETE)) { if (socketData->IsValid()) { ResetSocket(socketData); } error = ERROR_INTERNET_EXTENDED_ERROR; goto error_exit; } // // Parse out file size if its around // Assumes the substring containing the file size looks like; "(99999 bytes)" // if(!(lpSessionInfo->Flags & FFTP_KNOWN_FILE_SIZE)) { // Don't already have the size pch = (PCHAR)InternetLockErrorText(); if (pch != NULL) { pch = strstr(pch, "150 "); if (pch != NULL) { pch += sizeof("150 ") - 1; PCHAR pchEnd = strstr(pch, " bytes)"); if (pchEnd) { DWORD dwFileSize = 0; DWORD dwMul = 1; PCHAR pchBeg = pch; pch = pchEnd - 1; while (pch && (pch > pchBeg) && isdigit(*pch)) { dwFileSize += ((*pch - '0') * dwMul); dwMul *= 10; pch--; } if (pch && (*pch == '(') && (dwFileSize > 0)) { lpSessionInfo->dwFileSizeLow = dwFileSize; lpSessionInfo->Flags |= FFTP_KNOWN_FILE_SIZE; } } } } } // // unlock the response text // //InternetUnlockErrorText(); // // if we are expecting the server to create the connection (ACTIVE mode) // then perform an accept() on our listening socket in order to establish // the connection // if (!bPassiveMode) { // // if we just accept(), we may hang forever if the server doesn't call // back (servers are all alike), so we use select() to wait for the // socket to be acceptable before calling accept() proper // error = socketListener->SelectAccept( *socketData, // // BUGBUG - call GetTimeoutValue() // GlobalFtpAcceptTimeout ); if ( error != ERROR_SUCCESS ) { goto error_exit; } // // we no longer require the socket we created for listening for the // incoming server connection request // INET_ASSERT(socketListener != socketControl); INET_ASSERT(socketListener != socketData); INET_ASSERT(socketListener->GetSocket() != socketControl->GetSocket()); INET_ASSERT(socketListener->GetSocket() != socketData->GetSocket()); #if INET_DEBUG socketData->SetSourcePort(); #endif socketListener->Close(); if (!socketData->IsValid()) { goto error_exit; } } // // set send and receive timeouts for data socket. If we get an error, ignore // it. Note this probably means that the socket is (somehow) invalid, and // that we should really return an error. But for now (as with the other // protocols), I will presume that the error is non-fatal (but note it) // socketData->SetTimeout( SEND_TIMEOUT, (int)GetTimeoutValue(INTERNET_OPTION_DATA_SEND_TIMEOUT) ); socketData->SetTimeout( RECEIVE_TIMEOUT, (int)GetTimeoutValue(INTERNET_OPTION_DATA_RECEIVE_TIMEOUT) ); INET_ASSERT(error == ERROR_SUCCESS); INET_ASSERT(lpSessionInfo->socketData == socketData); quit: DEBUG_LEAVE(error); return error; error_exit: if (socketData->IsValid()) { socketData->Close(); } if (socketListener->IsValid()) { socketListener->Close(); } goto quit; }