You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5864 lines
161 KiB
5864 lines
161 KiB
/**********************************************************************/
|
|
/** Microsoft Windows NT **/
|
|
/** Copyright(c) Microsoft Corp., 1993 **/
|
|
/**********************************************************************/
|
|
|
|
/*
|
|
userdb.cxx
|
|
|
|
This module manages the user database for the FTPD Service.
|
|
|
|
Functions exported by this module:
|
|
|
|
|
|
DisconnectUser()
|
|
DisconnectUsersWithNoAccess()
|
|
EnumerateUsers()
|
|
|
|
USER_DATA::USER_DATA()
|
|
USER_DATA::Reset()
|
|
USER_DATA::~USER_DATA()
|
|
USER_DATA::Cleanup()
|
|
USER_DATA::ProcessAsyncIoCompletion()
|
|
USER_DATA::ReInitializeForNewUser()
|
|
USER_DATA::ReadCommand()
|
|
USER_DATA::DisconnectUserWithError()
|
|
USER_DATA::SendMultilineMessage()
|
|
USER_DATA::SendDirectoryAnnotation()
|
|
USER_DATA::GetFileSize();
|
|
|
|
ProcessUserAsyncIoCompletion()
|
|
|
|
FILE HISTORY:
|
|
KeithMo 07-Mar-1993 Created.
|
|
MuraliK March-May, 1995
|
|
Adding support for Async Io/Transfers
|
|
+ new USER_DATA class functions defined.
|
|
+ oob_inline enabled; ReadCommand() issued after
|
|
data socket is established.
|
|
+ added member functions for common operations
|
|
+ added ProcessAsyncIoCompletion()
|
|
+ added Establish & Destroy of Data connection
|
|
|
|
MuraliK 26-July-1995 Added Allocation caching of client conns.
|
|
Terryk 18-Sep-1996 Added GetFileSize
|
|
AMallet Sep 1998 Added support for AcceptEx() of PASV data connections
|
|
*/
|
|
|
|
|
|
#include "ftpdp.hxx"
|
|
#include <security.h>
|
|
# include "tsunami.hxx"
|
|
#include <timer.h>
|
|
# include "auxctrs.h"
|
|
# include <mbstring.h>
|
|
#include "acptctxt.hxx"
|
|
|
|
#define FIRST_TELNET_COMMAND 240
|
|
#define TELNET_DM_COMMAND 242
|
|
#define TELNET_IP_COMMAND 244
|
|
#define TELNET_SB_CODE 250
|
|
#define TELNET_SB_CODE_MIN 251
|
|
#define TELNET_SB_CODE_MAX 254
|
|
#define TELNET_IAC_CODE 255
|
|
|
|
# define MAX_FILE_SIZE_SPEC ( 32)
|
|
|
|
|
|
//
|
|
// Private globals.
|
|
//
|
|
|
|
// This will be expanded into
|
|
// static const char PSZ_StringName[] = ActualString;
|
|
//
|
|
# define ConstantStringsForThisModule() \
|
|
CStrM( ANONYMOUS_NAME, "Anonymous") /* must match engine.cxx */ \
|
|
CStrM( DEFAULT_SUB_DIRECTORY, "Default" ) \
|
|
CStrM( SENT_VERB, "sent" ) \
|
|
CStrM( CONNECTION_CLOSED_VERB, "closed" ) \
|
|
CStrM( FILE_ERROR, "%s: %s" ) \
|
|
CStrM( TRANSFER_COMPLETE, "Transfer complete." ) \
|
|
CStrM( TRANSFER_ABORTED, "Connection closed; transfer aborted." ) \
|
|
CStrM( TRANSFER_STARTING, "Data connection already open; Transfer starting." ) \
|
|
CStrM( INSUFFICIENT_RESOURCES, "Insufficient system resources." ) \
|
|
CStrM( TOO_MANY_PASV_USERS, "Too many passive-mode users." ) \
|
|
CStrM( OPENING_DATA_CONNECTION, "Opening %s mode data connection for %s%s." ) \
|
|
CStrM( CANNOT_OPEN_DATA_CONNECTION, "Can't open data connection." ) \
|
|
CStrM( COMMAND_TOO_LONG, "Command was too long" ) \
|
|
CStrM( LOCALUSER_DIR, "LocalUser\\" ) \
|
|
CStrM( ANONYMOUS_DIR, "Public" ) \
|
|
CStrM( DUMMY_END, "DummyMsg. Add string before this one")
|
|
|
|
|
|
//
|
|
// Generate the strings ( these are private globals of this module).
|
|
//
|
|
|
|
# define CStrM( StringName, ActualString) \
|
|
static const char PSZ_ ## StringName[] = ActualString ;
|
|
|
|
ConstantStringsForThisModule()
|
|
|
|
# undef CStrM
|
|
|
|
|
|
static DWORD p_NextUserId = 0; // Next available user id.
|
|
|
|
|
|
//
|
|
// Private prototypes.
|
|
//
|
|
|
|
DWORD
|
|
UserpGetNextId(
|
|
VOID
|
|
);
|
|
|
|
|
|
|
|
inline VOID
|
|
StopControlRead( IN LPUSER_DATA pUserData)
|
|
/*++
|
|
Stops control read operation, if one is proceeding.
|
|
Resets the CONTROL_READ flag as well as decrements ref count in user data.
|
|
|
|
--*/
|
|
{
|
|
if ( TEST_UF( pUserData, CONTROL_READ)) {
|
|
|
|
if ( InterlockedDecrement( &pUserData->m_nControlRead) < 0 ) {
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"StopControLRead: no read active!!!\n"));
|
|
DBG_ASSERT( FALSE);
|
|
}
|
|
|
|
DBG_REQUIRE( pUserData->DeReference() > 0);
|
|
CLEAR_UF( pUserData, CONTROL_READ);
|
|
}
|
|
|
|
} // StopControlRead()
|
|
|
|
BOOL
|
|
FilterTelnetCommands(IN CHAR * pszLine, IN DWORD cchLine,
|
|
IN LPBOOL pfLineEnded,
|
|
IN LPDWORD pcchRequestRecvd)
|
|
/*++
|
|
Filters out the Telnet commands and
|
|
terminates the command line with linefeed.
|
|
|
|
Also this function filters out the out of band data.
|
|
This works similar to the Sockutil.cxx::DiscardOutOfBandData().
|
|
We scan for the pattern "ABOR\r\n" and
|
|
set the OOB_DATA flag if it is present.
|
|
|
|
Arguments:
|
|
pszLine pointer to null terminated string containing the input data.
|
|
cchLine count of characters of data received
|
|
pfLineEnded pointer to Boolean flag which is set to true if complete
|
|
line has been received.
|
|
pcchRequestRecvd pointer to DWORD which on return contains the number
|
|
of bytes received.
|
|
|
|
Returns:
|
|
TRUE if the filtering is successful without any out of band abort request.
|
|
FALSE if there was any abort request in the input.
|
|
|
|
--*/
|
|
{
|
|
BOOL fDontAbort = TRUE;
|
|
BOOL fStateTelnetCmd = FALSE;
|
|
BOOL fStateTelnetSB = FALSE;
|
|
BOOL fFoundTelnetIP = FALSE;
|
|
CHAR * pszSrc;
|
|
CHAR * pszDst;
|
|
|
|
LPCSTR pszAbort = "ABOR\r\n";
|
|
LPCSTR pszNext = pszAbort;
|
|
|
|
DBG_ASSERT( pszLine != NULL && cchLine > 0 &&
|
|
pfLineEnded != NULL && pcchRequestRecvd != NULL);
|
|
|
|
*pfLineEnded = FALSE;
|
|
|
|
for( pszSrc = pszDst = pszLine; pszSrc < pszLine + cchLine && *pszSrc;
|
|
pszSrc++) {
|
|
|
|
CHAR ch = *pszSrc;
|
|
BYTE uch = (BYTE)ch;
|
|
|
|
//
|
|
// Filter out TELNET commands. these are of the form: IAC <cmd> or
|
|
// IAC SB <op> (IAC = 255, SB = 250, op 251..254, cmd > 240)
|
|
//
|
|
|
|
if( fStateTelnetCmd ) {
|
|
//
|
|
// we are in a Telbent command sequence
|
|
//
|
|
|
|
fStateTelnetCmd = FALSE;
|
|
|
|
DBG_ASSERT( uch >= FIRST_TELNET_COMMAND );
|
|
|
|
if( fStateTelnetSB ) {
|
|
//
|
|
// we are in a Telnet subsequence command
|
|
//
|
|
|
|
fStateTelnetSB = FALSE;
|
|
|
|
DBG_ASSERT( (uch >= TELNET_SB_CODE_MIN) &&
|
|
(uch <= TELNET_SB_CODE_MAX) );
|
|
|
|
if( uch >= FIRST_TELNET_COMMAND ) {
|
|
//
|
|
// consider it a valid Telnet command, as long as it's in
|
|
// the Telnet range. Filter this char out.
|
|
//
|
|
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// this is a TELNET protocol error, we'll ignore it.
|
|
//
|
|
// fall through with this char
|
|
//
|
|
|
|
} else if( uch == TELNET_SB_CODE ) {
|
|
//
|
|
// enter Telnet subsequense command state
|
|
//
|
|
|
|
fStateTelnetCmd = fStateTelnetSB = TRUE;
|
|
continue;
|
|
|
|
} else if( uch == TELNET_IAC_CODE ) {
|
|
//
|
|
// this is an escape sequence for a 255 data byte
|
|
//
|
|
// let it fall through
|
|
//
|
|
|
|
} else if ( uch == TELNET_IP_COMMAND ) {
|
|
//
|
|
// remember this, it is the first in a SYNCH sequence
|
|
//
|
|
|
|
fFoundTelnetIP = TRUE;
|
|
continue;
|
|
|
|
} else if ( uch == TELNET_DM_COMMAND ) {
|
|
//
|
|
// if in a SYNCH sequence, this resets the input stream
|
|
//
|
|
|
|
if( fFoundTelnetIP ) {
|
|
pszDst = pszLine;
|
|
fFoundTelnetIP = FALSE; // completed the SYNCH sequence
|
|
}
|
|
continue;
|
|
|
|
} else {
|
|
//
|
|
// we expect a Telnet command code here. filter it out
|
|
//
|
|
|
|
DBG_ASSERT( uch >= FIRST_TELNET_COMMAND );
|
|
|
|
if ( uch >= FIRST_TELNET_COMMAND ) {
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// this is a TELNET protocol error, we'll ignore it.
|
|
//
|
|
// fall through with this char
|
|
//
|
|
}
|
|
} else if( uch == TELNET_IAC_CODE ) {
|
|
//
|
|
// entering Telnet command parsing state
|
|
//
|
|
|
|
fStateTelnetCmd = TRUE;
|
|
continue;
|
|
} else if( uch == TELNET_DM_COMMAND ) {
|
|
//
|
|
// FTP.EXE on Win2k is sending an unexpected SYNCH sequence: DM, IAC, IP. See if this is it.
|
|
//
|
|
|
|
if( ( pszSrc == pszLine ) &&
|
|
( cchLine >= 3 ) &&
|
|
( (UINT)*(pszSrc+1) == TELNET_IAC_CODE ) &&
|
|
( (UINT)*(pszSrc+2) == TELNET_DM_COMMAND ) ) {
|
|
//
|
|
// just filter the sequence out
|
|
//
|
|
|
|
pszSrc += 2;
|
|
|
|
continue;
|
|
|
|
} else if( fFoundTelnetIP ) {
|
|
//
|
|
// or, it could be a single byte URGENT notification in the telnet Sync
|
|
//
|
|
|
|
pszDst = pszLine;
|
|
|
|
fFoundTelnetIP = FALSE; // completed the SYNCH sequence
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//
|
|
// if we have seen a Telnet IP, then skip everything up to a DM
|
|
//
|
|
|
|
if (fFoundTelnetIP) {
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// try matching ABOR\r\n
|
|
//
|
|
|
|
if ( *pszNext != ch) {
|
|
|
|
// the pattern match failed. reset to start at the beginning.
|
|
|
|
pszNext = pszAbort;
|
|
}
|
|
|
|
if ( *pszNext == ch) {
|
|
|
|
// pattern match at this character. move forward
|
|
pszNext++;
|
|
|
|
if ( *pszNext == '\0') { // end of string==> all matched.
|
|
|
|
// only consider this an OOB Abort if at the beginning of
|
|
// a (reset) line
|
|
|
|
if( (pszDst - pszLine + 2) == (pszNext - pszAbort) ) {
|
|
fDontAbort = FALSE;
|
|
}
|
|
|
|
pszNext = pszAbort;
|
|
}
|
|
}
|
|
|
|
//
|
|
// don't copy <CR> and <LF> to the output
|
|
//
|
|
|
|
if ( (ch != '\r') && ( ch != '\n')) {
|
|
|
|
*pszDst++ = ch;
|
|
|
|
} else if ( ch == '\n') {
|
|
|
|
// terminate at the linefeed
|
|
*pfLineEnded = TRUE;
|
|
break;
|
|
}
|
|
|
|
} // for
|
|
|
|
*pszDst = '\0';
|
|
|
|
*pcchRequestRecvd = DIFF(pszDst - pszLine);
|
|
DBG_ASSERT( *pcchRequestRecvd <= cchLine);
|
|
|
|
return (fDontAbort);
|
|
|
|
} // FilterTelnetCommands()
|
|
|
|
|
|
|
|
//
|
|
// Public functions.
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
USER_DATA::USER_DATA(
|
|
IN FTP_SERVER_INSTANCE *pInstance
|
|
)
|
|
/*++
|
|
This function creates a new UserData object for the information
|
|
required to process requests from a new User connection ( FTP).
|
|
|
|
Arguments:
|
|
sControl Socket used for control channel in FTP connection
|
|
clientIpAddress strcuture containing the client Ip address
|
|
|
|
Returns:
|
|
a newly constructed USER_DATA object.
|
|
Check IsValid() to ensure the object was properly created.
|
|
|
|
NOTE:
|
|
This function is to be used for dummy creation of the object so
|
|
allocation cacher can use this object.
|
|
Fields are randomly initialized. Reset() will initialize them properly.
|
|
|
|
However when a new effective USER_DATA object is needed, after allocation
|
|
one can call USER_DATA::Reset() to initialize all vars.
|
|
--*/
|
|
:
|
|
m_References ( 0),
|
|
m_ActiveRefAdded ( 0),
|
|
m_cchRecvBuffer ( 0),
|
|
m_licbRecvd ( 0),
|
|
m_cchPartialReqRecvd ( 0),
|
|
m_pOpenFileInfo ( NULL),
|
|
Flags ( 0),
|
|
UserToken ( NULL),
|
|
m_UserId ( 0),
|
|
DataPort ( 0),
|
|
UserState ( UserStateEmbryonic),
|
|
m_fOnActiveConnectionList ( FALSE),
|
|
m_AioControlConnection ( ProcessUserAsyncIoCompletion),
|
|
m_AioDataConnection ( ProcessUserAsyncIoCompletion),
|
|
m_AioDataFile ( ProcessUserAsyncIoCompletion),
|
|
m_AsyncTransferBuff ( NULL),
|
|
m_sPassiveDataListen ( INVALID_SOCKET),
|
|
CurrentDirHandle ( INVALID_HANDLE_VALUE),
|
|
RenameSourceBuffer ( NULL),
|
|
m_fCleanedup ( TRUE),
|
|
m_pMetaData ( NULL),
|
|
m_pInstance ( pInstance ),
|
|
m_acCheck ( AC_NOT_CHECKED ),
|
|
m_fNeedDnsCheck ( FALSE ),
|
|
m_dwLastReplyCode ( 0 ),
|
|
m_fHavePASVConn ( FALSE ),
|
|
m_fWaitingForPASVConn ( FALSE ),
|
|
m_fFakeIOCompletion ( FALSE ),
|
|
m_pszCmd ( NULL ),
|
|
m_dwRootDirStatus ( ERROR_ACCESS_DENIED ),
|
|
m_pAdioReq ( NULL ),
|
|
m_hPASVAcceptEvent ( NULL )
|
|
#if DBG
|
|
,m_RefTraceLog( NULL )
|
|
#endif
|
|
{
|
|
DWORD dwTimeout = m_pInstance->QueryConnectionTimeout();
|
|
|
|
INITIALIZE_CRITICAL_SECTION( &m_UserLock );
|
|
|
|
//
|
|
// Setup the structure signature. Until Reset(), it is invalid.
|
|
//
|
|
|
|
KILL_USER_SIG( this );
|
|
|
|
m_AioControlConnection.SetAioInformation( this, dwTimeout);
|
|
m_AioDataConnection.SetAioInformation( this, dwTimeout);
|
|
m_AioDataFile.SetAioInformation( this, dwTimeout);
|
|
|
|
InitializeListHead( &ListEntry);
|
|
|
|
ZeroMemory( m_recvBuffer, sizeof(m_recvBuffer));
|
|
|
|
IF_DEBUG( USER_DATABASE ) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"user_data object created @ %08lX.\n",
|
|
this));
|
|
}
|
|
|
|
m_licbSent = 0;
|
|
|
|
#if DBG
|
|
m_RefTraceLog = CreateRefTraceLog( TRACE_LOG_SIZE, 0 );
|
|
#endif
|
|
|
|
FakeIOTimes = 0;
|
|
|
|
} // USER_DATA::USER_DATA()
|
|
|
|
|
|
|
|
USER_DATA::~USER_DATA(VOID)
|
|
{
|
|
|
|
Cleanup();
|
|
|
|
if( RenameSourceBuffer != NULL ) {
|
|
TCP_FREE( RenameSourceBuffer);
|
|
RenameSourceBuffer = NULL;
|
|
}
|
|
|
|
if ( m_pszCmd != NULL )
|
|
{
|
|
TCP_FREE( m_pszCmd );
|
|
m_pszCmd = NULL;
|
|
}
|
|
|
|
if ( m_hPASVAcceptEvent != NULL )
|
|
{
|
|
RemovePASVAcceptEvent( TRUE );
|
|
}
|
|
|
|
if ( m_pInstance != NULL ) {
|
|
m_pInstance->DecrementCurrentConnections();
|
|
m_pInstance->Dereference();
|
|
m_pInstance = NULL;
|
|
}
|
|
|
|
#if DBG
|
|
if( m_RefTraceLog != NULL ) {
|
|
DestroyRefTraceLog( m_RefTraceLog );
|
|
}
|
|
#endif
|
|
|
|
DeleteCriticalSection( &m_UserLock );
|
|
|
|
} // USER_DATA::~USER_DATA()
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::Reset(IN SOCKET sControl,
|
|
IN PVOID EndpointObject,
|
|
IN IN_ADDR clientIpAddress,
|
|
IN const SOCKADDR_IN * psockAddrLocal /* = NULL */ ,
|
|
IN PATQ_CONTEXT pAtqContext /* = NULL */ ,
|
|
IN PVOID pvInitialRequest /* = NULL */ ,
|
|
IN DWORD cbWritten /* = 0 */ ,
|
|
IN AC_RESULT acCheck
|
|
)
|
|
{
|
|
BOOL fReturn = TRUE;
|
|
|
|
//
|
|
// Setup the structure signature.
|
|
//
|
|
|
|
INIT_USER_SIG( this );
|
|
|
|
m_References = 1; // set to 1 to prevent immediate deletion.
|
|
m_ActiveRefAdded= 1;
|
|
m_fCleanedup = FALSE;
|
|
Flags = m_pInstance->QueryUserFlags();
|
|
UserState = UserStateEmbryonic;
|
|
|
|
#if DBG
|
|
if( m_RefTraceLog != NULL ) {
|
|
ResetTraceLog( m_RefTraceLog );
|
|
}
|
|
#endif
|
|
|
|
m_pOpenFileInfo = NULL;
|
|
UserToken = NULL;
|
|
if ( m_pMetaData != NULL )
|
|
{
|
|
TsFreeMetaData( m_pMetaData->QueryCacheInfo() );
|
|
m_pMetaData = NULL;
|
|
}
|
|
m_UserId = UserpGetNextId();
|
|
m_xferType = XferTypeAscii;
|
|
m_xferMode = XferModeStream;
|
|
m_msStartingTime= 0;
|
|
m_acCheck = acCheck;
|
|
m_fNeedDnsCheck = FALSE;
|
|
m_dwLastReplyCode = 0;
|
|
|
|
HostIpAddress = clientIpAddress;
|
|
DataIpAddress = clientIpAddress;
|
|
|
|
m_licbRecvd = 0;
|
|
m_cchRecvBuffer = sizeof( m_recvBuffer) - sizeof(m_recvBuffer[0]);
|
|
m_cchPartialReqRecvd = 0;
|
|
|
|
CurrentDirHandle = INVALID_HANDLE_VALUE;
|
|
RenameSourceBuffer = NULL;
|
|
m_TimeAtConnection = GetCurrentTimeInSeconds();
|
|
m_TimeAtLastAccess = m_TimeAtConnection;
|
|
|
|
|
|
m_pvInitialRequest = pvInitialRequest;
|
|
m_cbInitialRequest = cbWritten;
|
|
|
|
//
|
|
// clean up the stuff needed to deal async with PASV command
|
|
//
|
|
if ( m_pszCmd )
|
|
{
|
|
TCP_FREE( m_pszCmd );
|
|
m_pszCmd = NULL;
|
|
}
|
|
|
|
if ( m_hPASVAcceptEvent )
|
|
{
|
|
RemovePASVAcceptEvent( TRUE );
|
|
}
|
|
|
|
CleanupPASVFlags();
|
|
|
|
// set up the async io contexts
|
|
m_AioControlConnection.SetNewSocket( sControl, pAtqContext, EndpointObject );
|
|
m_AioDataConnection.SetNewSocket(INVALID_SOCKET);
|
|
m_AioDataFile.SetNewSocket(INVALID_SOCKET);
|
|
m_sPassiveDataListen = ( INVALID_SOCKET);
|
|
if (m_AsyncTransferBuff) {
|
|
TCP_FREE( m_AsyncTransferBuff);
|
|
m_AsyncTransferBuff = NULL;
|
|
}
|
|
|
|
m_rgchFile[0] = '\0';
|
|
m_szUserName[0] = '\0'; // no user name available yet.
|
|
m_szCurrentDirectory.Reset(); // initialize to no virtual dir.
|
|
m_dwRootDirStatus = ERROR_ACCESS_DENIED;
|
|
m_pAdioReq = NULL;
|
|
m_strRootDir.Reset(); // no root directory known yet
|
|
|
|
m_licbSent = 0;
|
|
|
|
m_pInstance->QueryStatsObj()->IncrCurrentConnections();
|
|
|
|
m_liCurrentOffset = 0;
|
|
m_liNextOffset = 0;
|
|
|
|
|
|
//
|
|
// get the local Ip address
|
|
//
|
|
|
|
if ( psockAddrLocal != NULL) {
|
|
|
|
LocalIpAddress = psockAddrLocal->sin_addr;
|
|
LocalIpPort = psockAddrLocal->sin_port;
|
|
} else {
|
|
|
|
SOCKADDR_IN saddrLocal;
|
|
INT cbLocal;
|
|
|
|
cbLocal = sizeof( saddrLocal);
|
|
if ( getsockname( sControl, (SOCKADDR *) &saddrLocal, &cbLocal) != 0) {
|
|
|
|
DWORD err = WSAGetLastError();
|
|
|
|
fReturn = FALSE;
|
|
|
|
IF_DEBUG( ERROR) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Failure in getsockname( sock=%d). Error = %u\n",
|
|
sControl, err));
|
|
}
|
|
|
|
SetLastError( err);
|
|
|
|
} else {
|
|
|
|
LocalIpAddress = saddrLocal.sin_addr;
|
|
LocalIpPort = saddrLocal.sin_port;
|
|
}
|
|
}
|
|
|
|
DataPort = CONN_PORT_TO_DATA_PORT(LocalIpPort);
|
|
|
|
//
|
|
// Success!
|
|
//
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
time_t now;
|
|
time( & now);
|
|
CHAR pchAddr[32];
|
|
|
|
InetNtoa( clientIpAddress, pchAddr);
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Client Connection for %s:%d starting @ %s",
|
|
pchAddr, sControl,
|
|
asctime( localtime( &now))));
|
|
}
|
|
|
|
IF_DEBUG( USER_DATABASE ) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"user %lu reset @ %08lX.\n",
|
|
QueryId(), this));
|
|
}
|
|
|
|
m_nControlRead = 0;
|
|
|
|
FakeIOTimes = 0;
|
|
|
|
return (fReturn);
|
|
|
|
} // USER_DATA::Reset()
|
|
|
|
|
|
|
|
|
|
VOID
|
|
USER_DATA::Cleanup( VOID)
|
|
/*++
|
|
This cleans up data stored in the user data object.
|
|
|
|
Returns:
|
|
None
|
|
|
|
--*/
|
|
{
|
|
DBG_ASSERT( QueryReference() == 0);
|
|
|
|
if ( m_fCleanedup) {
|
|
return;
|
|
}
|
|
|
|
if ( m_pMetaData != NULL )
|
|
{
|
|
TsFreeMetaData( m_pMetaData->QueryCacheInfo() );
|
|
m_pMetaData = NULL;
|
|
}
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( this)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
IF_DEBUG( USER_DATABASE ) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
" Cleaning up user %lu @ %08lX.\n",
|
|
QueryId(), this));
|
|
}
|
|
|
|
DBG_ASSERT( m_nControlRead == 0);
|
|
|
|
//
|
|
// Clean up stuff needed to deal with PASV connections
|
|
//
|
|
if ( m_hPASVAcceptEvent )
|
|
{
|
|
RemovePASVAcceptEvent( TRUE );
|
|
}
|
|
|
|
|
|
if ( m_pszCmd )
|
|
{
|
|
TCP_FREE( m_pszCmd );
|
|
m_pszCmd = NULL;
|
|
}
|
|
|
|
//
|
|
// Close any open sockets & handles.
|
|
//
|
|
|
|
CloseSockets( FALSE );
|
|
|
|
// invalidate the connections
|
|
m_AioControlConnection.SetNewSocket(INVALID_SOCKET);
|
|
m_AioDataConnection.SetNewSocket(INVALID_SOCKET);
|
|
m_AioDataFile.SetNewSocket(INVALID_SOCKET);
|
|
if (m_AsyncTransferBuff) {
|
|
TCP_FREE( m_AsyncTransferBuff);
|
|
m_AsyncTransferBuff = NULL;
|
|
}
|
|
|
|
|
|
//
|
|
// Update the statistics.
|
|
//
|
|
|
|
if( IsLoggedOn()
|
|
&& !TEST_UF( this, WAIT_PASS ) )
|
|
{
|
|
if( TEST_UF( this, ANONYMOUS))
|
|
{
|
|
m_pInstance->QueryStatsObj()->DecrCurrentAnonymousUsers();
|
|
}
|
|
else
|
|
{
|
|
m_pInstance->QueryStatsObj()->DecrCurrentNonAnonymousUsers();
|
|
}
|
|
}
|
|
|
|
m_pInstance->QueryStatsObj()->DecrCurrentConnections();
|
|
|
|
if( UserToken != NULL )
|
|
{
|
|
TsDeleteUserToken( UserToken );
|
|
UserToken = NULL;
|
|
}
|
|
|
|
if( CurrentDirHandle != INVALID_HANDLE_VALUE )
|
|
{
|
|
IF_DEBUG( VIRTUAL_IO )
|
|
{
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"closing directory handle %08lX\n",
|
|
CurrentDirHandle ));
|
|
}
|
|
|
|
CloseHandle( CurrentDirHandle );
|
|
CurrentDirHandle = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
if ( m_pOpenFileInfo != NULL) {
|
|
|
|
DBG_REQUIRE( CloseFileForSend());
|
|
}
|
|
|
|
DBG_REQUIRE( CloseFileForReceive());
|
|
|
|
//
|
|
// Release the memory attached to this structure.
|
|
//
|
|
|
|
|
|
if( RenameSourceBuffer != NULL ) {
|
|
|
|
// do not free this location until end of usage.
|
|
RenameSourceBuffer[0] = '\0';
|
|
}
|
|
|
|
m_UserId = 0; // invalid User Id
|
|
|
|
//
|
|
// Kill the structure signature.
|
|
//
|
|
|
|
KILL_USER_SIG( this );
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
time_t now;
|
|
time( & now);
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Client Connection for %s:%d ending @ %s",
|
|
inet_ntoa( HostIpAddress), QueryControlSocket(),
|
|
asctime( localtime( &now))));
|
|
}
|
|
|
|
|
|
//
|
|
// There is a possible race condition. If the socket was abruptly closed
|
|
// and there was any pending Io, they will get blown away. This will
|
|
// cause a call-back from the ATQ layer. That is unavoidable.
|
|
// In such cases it is possible that the object was deleted.
|
|
// This can lead to problems. We need to be careful.
|
|
// But Reference Count protects against such disasters. So tread
|
|
// carefully and use Reference count.
|
|
//
|
|
|
|
DBG_ASSERT( m_sPassiveDataListen == INVALID_SOCKET);
|
|
|
|
m_fCleanedup = TRUE; // since we just cleaned up this object
|
|
|
|
return;
|
|
|
|
} // USER_DATA::Cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VOID
|
|
USER_DATA::ReInitializeForNewUser( VOID)
|
|
/*++
|
|
|
|
This function reinitializes the user data information for a new user to
|
|
communicate with the server using existing control socket connection.
|
|
|
|
--*/
|
|
{
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( this)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
//
|
|
// Update the statistics.
|
|
//
|
|
|
|
if( IsLoggedOn())
|
|
{
|
|
if( TEST_UF( this, ANONYMOUS))
|
|
{
|
|
m_pInstance->QueryStatsObj()->DecrCurrentAnonymousUsers();
|
|
}
|
|
else
|
|
{
|
|
m_pInstance->QueryStatsObj()->DecrCurrentNonAnonymousUsers();
|
|
}
|
|
}
|
|
|
|
CLEAR_UF_BITS( this, (UF_LOGGED_ON | UF_ANONYMOUS | UF_PASSIVE));
|
|
|
|
LockUser();
|
|
if( QueryState() != UserStateDisconnected ) {
|
|
SetState( UserStateWaitingForUser );
|
|
}
|
|
UnlockUser();
|
|
|
|
if ( m_pMetaData != NULL )
|
|
{
|
|
TsFreeMetaData( m_pMetaData->QueryCacheInfo() );
|
|
m_pMetaData = NULL;
|
|
}
|
|
m_TimeAtConnection= GetCurrentTimeInSeconds();
|
|
m_TimeAtLastAccess= m_TimeAtConnection;
|
|
m_xferType = XferTypeAscii;
|
|
m_xferMode = XferModeStream;
|
|
DataIpAddress = HostIpAddress;
|
|
DataPort = CONN_PORT_TO_DATA_PORT(LocalIpPort);
|
|
|
|
m_szUserName[0] = '\0';
|
|
m_szCurrentDirectory.Reset();
|
|
m_strRootDir.Reset();
|
|
|
|
if( UserToken != NULL )
|
|
{
|
|
TsDeleteUserToken( UserToken );
|
|
UserToken = NULL;
|
|
}
|
|
|
|
if( CurrentDirHandle != INVALID_HANDLE_VALUE )
|
|
{
|
|
IF_DEBUG( VIRTUAL_IO )
|
|
{
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"closing directory handle %08lX\n",
|
|
CurrentDirHandle ));
|
|
}
|
|
|
|
CloseHandle( CurrentDirHandle );
|
|
CurrentDirHandle = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
if ( m_pOpenFileInfo != NULL) {
|
|
|
|
DBG_REQUIRE( CloseFileForSend());
|
|
}
|
|
|
|
DBG_REQUIRE( CloseFileForReceive());
|
|
|
|
m_licbSent = 0;
|
|
|
|
m_pvInitialRequest = NULL;
|
|
m_cbInitialRequest = 0;
|
|
|
|
CleanupPassiveSocket( TRUE );
|
|
|
|
return;
|
|
|
|
} // USER_DATA::ReInitializeForNewUser()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::ProcessAsyncIoCompletion(
|
|
IN DWORD cbIo,
|
|
IN DWORD dwError,
|
|
IN LPASYNC_IO_CONNECTION pAioConn,
|
|
IN BOOL fTimedOut)
|
|
/*++
|
|
This function processes the Async Io completion.
|
|
( invoked due to a callback from the ASYNC_IO_CONNECTION object)
|
|
|
|
Arguments:
|
|
pContext pointer to the context information ( UserData object).
|
|
cbIo count of bytes transferred in Io
|
|
dwError DWORD containing the error code resulting from last tfr.
|
|
pAioConn pointer to AsyncIo connection object.
|
|
fTimedOut flag indicating if the current call was made
|
|
because of timeout.
|
|
|
|
Returns:
|
|
None
|
|
--*/
|
|
{
|
|
BOOL fReturn = FALSE;
|
|
AC_RESULT acDnsAccess;
|
|
DWORD dwOriginalError;
|
|
|
|
dwOriginalError = dwError;
|
|
|
|
//
|
|
// Special processing if it's an IO completion on the control connection - we might
|
|
// be processing a completion we posted ourselves to signal that the data socket for the PASV
|
|
// data connection is now accept()'able.
|
|
//
|
|
|
|
if ( pAioConn == &m_AioControlConnection && QueryInFakeIOCompletion() )
|
|
{
|
|
// Here is a horrible race condition:
|
|
// If the FTP client closes the control socket
|
|
// right after having finished receiving the transmitted file
|
|
// than there may be a thread that enters this
|
|
// code path (because the FakeIO flag is set, and the
|
|
// Control Socket is involved) before the IO completion
|
|
// for the data connection has traveled this same function,
|
|
// cleaning the FakeIO flag
|
|
// Here is the race condition:
|
|
// A thread enter here, and see that the FakeIO is set
|
|
// the normal behavior is reprocessing a command like
|
|
// "RETR foo.txt", while now the command if a zero length string.
|
|
// the Second thread enter this function with the DataConnection
|
|
// it clears the flag (at a non specified point of the
|
|
// processing of the other thread) and it exit.
|
|
// the original thread is now processing a saved string
|
|
// (because of the FakeIO flag) while it is not supposed to.
|
|
// this causes problems to the time-out algorithm, because
|
|
// of a ref-count problem in the USER_DATA
|
|
|
|
LONG CurVal = InterlockedIncrement(&(this->FakeIOTimes));
|
|
if (CurVal>1){
|
|
goto NormalProcessing;
|
|
}
|
|
|
|
//
|
|
// Remove the reference used to deal with the race condition between an IO
|
|
// thread doing clean-up and the thread watching for the data socket to become
|
|
// accept()'able and holding on to this USER_DATA object
|
|
//
|
|
DeReference();
|
|
|
|
//
|
|
// There is a race condition between the thread watching for a socket to become
|
|
// accept()'able and an IO thread being woken up because the client has (unexpectedly)
|
|
// disconnected. The thread watching the socket will post a fake IO completion to
|
|
// indicate that an accept() on the socket will succeed; however, if the client
|
|
// disconnects (the control connection) before the fake completion is processed,
|
|
// we don't want to do any more processing.
|
|
//
|
|
if ( UserState == UserStateDisconnected )
|
|
{
|
|
return TRUE;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Fast-path if we know this is the second time around we're trying to process the
|
|
// command, which happens when we're in PASV mode
|
|
//
|
|
DBG_ASSERT( ( UserState == UserStateLoggedOn ) ||
|
|
(( UserState == UserStateWaitingForPass ) && QueryInFakeIOCompletion()) );
|
|
goto ProcessCommand;
|
|
}
|
|
}
|
|
|
|
NormalProcessing:
|
|
|
|
if( dwError != NO_ERROR &&
|
|
dwError != ERROR_SEM_TIMEOUT )
|
|
{
|
|
|
|
//
|
|
// Geezsh, I hate my life.
|
|
//
|
|
// Once upon a time, there was a bug in ATQ that cause it to
|
|
// always pass NO_ERROR as the status to the async completion
|
|
// routine. This bug caused, among other things, FTP to never
|
|
// time out idle connections, because it never saw the
|
|
// ERROR_SEM_TIMEOUT status. So, I fixed the bug in ATQ.
|
|
//
|
|
// Now, this completion routine gets the actual status. Well,
|
|
// that breaks service shutdown when there are connected users.
|
|
// Basically, when a shutdown occurs, the connected sockets are
|
|
// closed, causing the IO to complete with ERROR_NETNAME_DELETED.
|
|
// USER_DATA::ProcessAsyncIoCompletion() is not handling this
|
|
// error properly, which causes 1) an assertion failure because
|
|
// USER_DATA::DisconnectUserWithError() is getting called *twice*
|
|
// and 2) the service never stops because of a dangling reference
|
|
// on the USER_DATA structure.
|
|
//
|
|
// Of course, the proper thing to do would be to fix the offending
|
|
// code in USER_DATA::ProcessAsyncIoCompletion() so that it DID
|
|
// handle the error properly. Unfortunately, that fix requires a
|
|
// nontrivial amount of surgery, and we're a scant three days
|
|
// from releasing K2 Beta 1. So...
|
|
//
|
|
// As a quick & dirty work around for K2 Beta 1, we'll map all
|
|
// errors other than ERROR_SEM_TIMEOUT to NO_ERROR. This should
|
|
// provide the lower software layers with the old ATQ behavior
|
|
// they're expecting.
|
|
//
|
|
// REMOVE THIS POST BETA 1 AND FIX USER_DATA PROPERLY!!!!
|
|
//
|
|
// 3/12/98
|
|
//
|
|
// N.B. The debug output below has been changed to be a little
|
|
// more customer friendly but I hate to prevent future developers
|
|
// for enjoying the original message which read:
|
|
// "Mapping error %d to NO_ERROR to mask FTP bug (FIX!)\n"
|
|
//
|
|
// I'm removing this message because it was the source of some
|
|
// embarrasment, when a checked version of this DLL was sent to
|
|
// Ernst & Young to track the now famous bug #138566.
|
|
//
|
|
|
|
DBGPRINTF((
|
|
DBG_CONTEXT,
|
|
"Mapping error %d to NO_ERROR\n",
|
|
dwError
|
|
));
|
|
|
|
dwError = NO_ERROR;
|
|
|
|
}
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( this)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
IF_DEBUG( USER_DATABASE) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"[%lu] Entering USER_DATA( %08x)::Process( %u, %u, %08x)."
|
|
" RefCount = %d. State = %d\n",
|
|
GetTickCount(),
|
|
this, cbIo, dwError, pAioConn, QueryReference(),
|
|
QueryState()));
|
|
}
|
|
|
|
if ( m_fNeedDnsCheck )
|
|
{
|
|
acDnsAccess = QueryAccessCheck()->CheckDnsAccess();
|
|
|
|
UnbindInstanceAccessCheck();
|
|
|
|
m_fNeedDnsCheck = FALSE;
|
|
|
|
if ( (acDnsAccess == AC_IN_DENY_LIST) ||
|
|
(acDnsAccess == AC_NOT_IN_GRANT_LIST) ||
|
|
((m_acCheck == AC_NOT_IN_GRANT_LIST) &&
|
|
(acDnsAccess != AC_IN_GRANT_LIST) ) ) {
|
|
|
|
ReplyToUser(this,
|
|
REPLY_NOT_LOGGED_IN,
|
|
"Connection refused, unknown IP address." );
|
|
|
|
DisconnectUserWithError( NO_ERROR );
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
if ( pAioConn == &m_AioDataConnection || pAioConn == &m_AioDataFile)
|
|
{
|
|
|
|
//
|
|
// a Data transfer operation has completed.
|
|
//
|
|
|
|
DBG_REQUIRE( IsLoggedOn());
|
|
|
|
// Update last access time
|
|
m_TimeAtLastAccess = GetCurrentTimeInSeconds();
|
|
|
|
if ( TEST_UF( this, ASYNC_UPLOAD)) {
|
|
//
|
|
// we are in the data pump loop for uploading a file, and have just completed
|
|
// receiving or writing a block of data. If there were no errors, just initiate an
|
|
// asynchronous write or receive for a block of data, and be done. otherwise, fall
|
|
// through to normal error handling.
|
|
//
|
|
|
|
BOOL fAsyncOK;
|
|
|
|
if ( (dwError != NO_ERROR) || (cbIo == 0)) {
|
|
|
|
//
|
|
// Either the transfer is complete, or an error has occured. terminate.
|
|
//
|
|
|
|
fAsyncOK = FALSE;
|
|
|
|
} else if ( TEST_UF( this, OOB_DATA)) {
|
|
|
|
//
|
|
// we got an out-of-band abbort command, so bail out.
|
|
//
|
|
|
|
fAsyncOK = FALSE;
|
|
dwOriginalError = ERROR_OPERATION_ABORTED;
|
|
|
|
} else if ( pAioConn == &m_AioDataConnection) {
|
|
|
|
//
|
|
// received another block. update stats and initiate async write to file
|
|
//
|
|
|
|
IncrementCbRecvd( cbIo);
|
|
|
|
fAsyncOK = m_AioDataFile.WriteFile( m_AsyncTransferBuff,
|
|
cbIo,
|
|
QueryCurrentOffset());
|
|
|
|
dwOriginalError = GetLastError();
|
|
} else {
|
|
|
|
//
|
|
// Write completed. update offset & start new receive
|
|
//
|
|
|
|
IncCurrentOffset( cbIo);
|
|
|
|
fAsyncOK = m_AioDataConnection.ReadFile(m_AsyncTransferBuff,
|
|
g_SocketBufferSize);
|
|
|
|
dwOriginalError = GetLastError();
|
|
}
|
|
|
|
if (fAsyncOK) {
|
|
|
|
//
|
|
// we have successfully initiated the next async operation, so we are done.
|
|
//
|
|
|
|
return TRUE;
|
|
}
|
|
} else if ( TEST_UF( this, ASYNC_DOWNLOAD)) {
|
|
//
|
|
// we are in the datapump for downloading a large file. initiate
|
|
// a transmit file for the next chunk.
|
|
//
|
|
|
|
BOOL fAsyncOK;
|
|
BOOL fDisconnectSocket;
|
|
|
|
if ( (dwError != NO_ERROR) || (cbIo < MAX_TRANSMIT_FILE_DATA_CHUNK)) {
|
|
|
|
//
|
|
// Either the transfer is complete, or an error has occured. terminate.
|
|
//
|
|
|
|
fAsyncOK = FALSE;
|
|
|
|
} else if ( TEST_UF( this, OOB_DATA)) {
|
|
|
|
//
|
|
// we got an out-of-band abbort command, so bail out.
|
|
//
|
|
|
|
fAsyncOK = FALSE;
|
|
dwOriginalError = ERROR_OPERATION_ABORTED;
|
|
|
|
} else {
|
|
|
|
LARGE_INTEGER FileSize;
|
|
|
|
IncrementCbSent( cbIo);
|
|
|
|
IncCurrentOffset( cbIo);
|
|
|
|
m_pOpenFileInfo->QuerySize( FileSize);
|
|
|
|
if (FileSize.QuadPart > QueryCurrentOffset()) {
|
|
|
|
//
|
|
// prepare to send the next chunk
|
|
//
|
|
|
|
FileSize.QuadPart -= QueryCurrentOffset();
|
|
|
|
if (FileSize.QuadPart > MAX_TRANSMIT_FILE_DATA_CHUNK) {
|
|
|
|
FileSize.QuadPart = MAX_TRANSMIT_FILE_DATA_CHUNK;
|
|
|
|
fDisconnectSocket = FALSE;
|
|
|
|
} else {
|
|
//
|
|
// there is nothing left to send after this chunk, so go through
|
|
// normal path at next completion. Also disconnect socket after
|
|
// this transmit
|
|
//
|
|
|
|
CLEAR_UF( this, ASYNC_DOWNLOAD);
|
|
|
|
fDisconnectSocket = TRUE;
|
|
}
|
|
|
|
fAsyncOK = m_AioDataConnection.TransmitFileTs( m_pOpenFileInfo,
|
|
FileSize,
|
|
QueryCurrentOffset(),
|
|
fDisconnectSocket);
|
|
|
|
dwOriginalError = GetLastError();
|
|
|
|
} else if (FileSize.QuadPart == QueryCurrentOffset()) {
|
|
|
|
//
|
|
// we have sent the entire file, done.
|
|
//
|
|
|
|
fAsyncOK = FALSE;
|
|
|
|
} else {
|
|
|
|
//
|
|
// the offset is larger than the file, this is an internal error condition
|
|
//
|
|
|
|
DBG_ASSERT( FileSize.QuadPart >= QueryCurrentOffset());
|
|
|
|
fAsyncOK = FALSE;
|
|
dwOriginalError = ERROR_INTERNAL_ERROR;
|
|
}
|
|
}
|
|
|
|
if (fAsyncOK) {
|
|
|
|
//
|
|
// we have successfully initiated the next async operation, so we are done.
|
|
//
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
if ( dwError == NO_ERROR || !fTimedOut)
|
|
{
|
|
|
|
// dwError == NO_ERROR ==> No error in transmitting data
|
|
// so decrease ref count and blow away the sockets.
|
|
|
|
// if dwError != NO_ERROR then
|
|
// if timeout occured ==> ATQ will send another callback
|
|
// so do not decrease ref count now.
|
|
// if no timeout ==> then decrement ref count now.
|
|
|
|
DBG_REQUIRE( DeReference() > 0);
|
|
}
|
|
else
|
|
{
|
|
// RobSol: 8/20/01: this looks like a faulty statement. fTimeOut must be TRUE, or the
|
|
// previous "if" would be true. I am leaving this dead code commented out, in case we
|
|
// find it was supposed to exist within some other logic.
|
|
// if ( fTimedOut)
|
|
// {
|
|
// SET_UF( this, DATA_TIMEDOUT);
|
|
// }
|
|
// else
|
|
// {
|
|
// SET_UF( this, DATA_ERROR);
|
|
// }
|
|
SET_UF( this, DATA_TIMEDOUT);
|
|
}
|
|
|
|
# ifdef CHECK_DBG
|
|
if ( dwError != NO_ERROR)
|
|
{
|
|
|
|
CHAR szBuffer[100];
|
|
DBG_REQUIRE( _snprintf( szBuffer, sizeof( szBuffer ),
|
|
" Data Socket Error = %u ", dwError) > 0);
|
|
Print( szBuffer);
|
|
}
|
|
# endif // CHECK_DBG
|
|
|
|
CLEAR_UF( this, ASYNC_TRANSFER);
|
|
CLEAR_UF( this, ASYNC_DOWNLOAD);
|
|
|
|
//
|
|
// Destroy the data connection.
|
|
// Send message accordingly to indicate if this was a failure/success
|
|
// That is done by DestroyDataConnection.
|
|
//
|
|
DBG_REQUIRE( DestroyDataConnection( dwOriginalError));
|
|
|
|
DBG_REQUIRE( CloseFileForReceive( dwOriginalError));
|
|
|
|
if ( m_pOpenFileInfo != NULL)
|
|
{
|
|
//
|
|
// set number of bytes actually sent
|
|
//
|
|
m_licbSent += cbIo;
|
|
|
|
DBG_REQUIRE( CloseFileForSend( dwOriginalError));
|
|
}
|
|
|
|
if ( dwError == NO_ERROR)
|
|
{
|
|
|
|
//
|
|
// Process any Pending commands, due to the parallel
|
|
// control channel operation for this user Connection.
|
|
// For the present, we dont buffer commands ==> No processing
|
|
// to be done effectively. NYI
|
|
// Just ensure that there is a read-operation pending on
|
|
// control channel.
|
|
//
|
|
|
|
// BOGUS: DBG_ASSERT( TEST_UF( this, CONTROL_READ));
|
|
}
|
|
|
|
fReturn = TRUE; // since this function went on well.
|
|
}
|
|
else if ( pAioConn == &m_AioControlConnection)
|
|
{
|
|
|
|
//
|
|
// a control socket operation has completed.
|
|
//
|
|
|
|
if ( dwError != NO_ERROR)
|
|
{
|
|
|
|
//
|
|
// There is an error in processing the control connection request.
|
|
// the only ASYNC_IO request we submit on control is:
|
|
// Read request on control socket
|
|
//
|
|
|
|
if ( fTimedOut)
|
|
{
|
|
|
|
if ( TEST_UF( this, TRANSFER))
|
|
{
|
|
|
|
// A data transfer is going on.
|
|
// allow client to send commands later
|
|
// (client may not be async in control/data io,so allow it)
|
|
|
|
// resubmit the control read operation
|
|
// after clearing old one
|
|
|
|
//
|
|
// Since there is a pending IO in atq.
|
|
// Just resume the timeout processing in ATQ for
|
|
// this context.
|
|
//
|
|
|
|
pAioConn->ResumeIoOperation();
|
|
fReturn = TRUE;
|
|
}
|
|
else
|
|
{
|
|
|
|
// For timeouts, ATQ sends two call backs.
|
|
// So be careful to decrement reference count only once.
|
|
|
|
DBG_ASSERT( fReturn == FALSE);
|
|
|
|
DBG_ASSERT( TEST_UF( this, CONTROL_READ));
|
|
SET_UF( this, CONTROL_TIMEDOUT);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
|
|
// Either there should be a control read pending or
|
|
// control socket should have received a timeout.
|
|
DBG_ASSERT( TEST_UF( this, CONTROL_READ) ||
|
|
TEST_UF( this, CONTROL_TIMEDOUT)
|
|
);
|
|
|
|
// a non-timeout error has occured. ==> stop read operation.
|
|
StopControlRead(this);
|
|
DBG_ASSERT( fReturn == FALSE);
|
|
SET_UF( this, CONTROL_ERROR);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
|
|
// If this connection had an outstanding IO on wait queue, it
|
|
// got completed. Hence get rid of the reference count.
|
|
StopControlRead( this);
|
|
|
|
switch ( UserState)
|
|
{
|
|
|
|
case UserStateEmbryonic:
|
|
|
|
fReturn = StartupSession( m_pvInitialRequest,
|
|
m_cbInitialRequest);
|
|
|
|
if ( m_pvInitialRequest == NULL)
|
|
{
|
|
// No initial buffer. Wait for read to complete
|
|
break;
|
|
}
|
|
|
|
cbIo = m_cbInitialRequest; // fake the bytes read.
|
|
|
|
// Fall Through for processing request
|
|
|
|
case UserStateWaitingForUser:
|
|
case UserStateWaitingForPass:
|
|
case UserStateLoggedOn:
|
|
|
|
ProcessCommand:
|
|
//
|
|
// Input already read. Process request and submit another read.
|
|
//
|
|
|
|
fReturn = ParseAndProcessRequest(cbIo/sizeof(CHAR));
|
|
|
|
if ( fReturn && IsDisconnected() &&
|
|
TEST_UF( this, CONTROL_TIMEDOUT))
|
|
{
|
|
|
|
// disconnect only if no pending control read
|
|
// if there is a pending control read,
|
|
// atq will pop this up for cleanup.
|
|
fReturn = !(TEST_UF( this, CONTROL_READ));
|
|
|
|
IF_DEBUG( ERROR) {
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"%08x ::Timeout killed conn while "
|
|
" processing!\n State = %d(%x),"
|
|
" Ref = %d, Id = %d, fRet=%d\n",
|
|
this, QueryState(), Flags,
|
|
QueryReference(), QueryId(), fReturn
|
|
));
|
|
}
|
|
FacIncrement( CacTimeoutWhenProcessing);
|
|
}
|
|
break;
|
|
|
|
case UserStateDisconnected:
|
|
|
|
fReturn = TRUE;
|
|
if ( TEST_UF( this, CONTROL_TIMEDOUT))
|
|
{
|
|
|
|
// Timeout thread raced against me :(
|
|
|
|
IF_DEBUG( ERROR) {
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"%08x :: Conn already Disconnected !!!\n"
|
|
" State = %d(%x), Ref = %d, Id = %d\n",
|
|
this, QueryState(), Flags,
|
|
QueryReference(), QueryId()
|
|
));
|
|
}
|
|
FacIncrement( CacTimeoutInDisconnect);
|
|
fReturn = FALSE;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
|
|
DBG_ASSERT( !"Invalid UserState for processing\n");
|
|
SetLastError( ERROR_INVALID_PARAMETER);
|
|
break;
|
|
} // switch
|
|
|
|
dwError = ( fReturn) ? NO_ERROR : GetLastError();
|
|
}
|
|
|
|
if ( !fReturn)
|
|
{
|
|
DisconnectUserWithError( dwError, fTimedOut);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
DBG_ASSERT( !"call to Process() with wrong parameters");
|
|
}
|
|
|
|
IF_DEBUG( USER_DATABASE) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"[%lu] Leaving USER_DATA( %08x)::Process()."
|
|
" RefCount = %d. State = %d\n",
|
|
GetTickCount(),
|
|
this, QueryReference(), QueryState())
|
|
);
|
|
}
|
|
|
|
return ( fReturn);
|
|
} // USER_DATA::ProcessAsyncIoCompletion()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# define min(a, b) (((a) < (b)) ? (a) : (b))
|
|
|
|
BOOL
|
|
USER_DATA::StartupSession(IN PVOID pvInitialRequest,
|
|
IN DWORD cbInitialRequest
|
|
)
|
|
/*++
|
|
This function allocates a buffer for receiving request from the client
|
|
and also sets up initial read from the control socket to
|
|
get client requests.
|
|
|
|
Arguments:
|
|
pvInitialRequest pointer to initial request buffer
|
|
cbInitialRequest count of bytes of data in the initial request
|
|
|
|
Returns:
|
|
TRUE on success and FALSE if there is any failure.
|
|
|
|
--*/
|
|
{
|
|
SOCKERR serr;
|
|
BOOL fReturn = FALSE;
|
|
PCSTR pszBanner;
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( this)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
DBG_ASSERT( QueryState() == UserStateEmbryonic);
|
|
|
|
//
|
|
// Reply to the initial connection message. ( Greet the new user).
|
|
//
|
|
|
|
pszBanner = QueryInstance()->QueryBannerMsg();
|
|
|
|
if( pszBanner && *pszBanner ) {
|
|
serr = SendMultilineMessage(
|
|
REPLY_SERVICE_READY,
|
|
g_FtpServiceNameString,
|
|
TRUE,
|
|
FALSE);
|
|
|
|
serr = serr || SendMultilineMessage(
|
|
REPLY_SERVICE_READY,
|
|
pszBanner,
|
|
FALSE,
|
|
TRUE);
|
|
} else {
|
|
serr = ReplyToUser( this,
|
|
REPLY_SERVICE_READY,
|
|
"%s",
|
|
g_FtpServiceNameString );
|
|
}
|
|
|
|
if ( serr != 0) {
|
|
|
|
IF_DEBUG( ERROR) {
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Cannot reply with initial connection message."
|
|
" Error = %lu\n",
|
|
serr));
|
|
}
|
|
|
|
} else {
|
|
|
|
//
|
|
// enable OOB_INLINE since we are using that for our control socket
|
|
//
|
|
BOOL fOobInline = TRUE;
|
|
|
|
serr = setsockopt( QueryControlSocket(), SOL_SOCKET,
|
|
SO_OOBINLINE, (const char *) &fOobInline,
|
|
sizeof( fOobInline));
|
|
|
|
m_cchPartialReqRecvd = 0;
|
|
|
|
if ( serr == 0) {
|
|
|
|
//
|
|
// Try to set up the buffer and enter the mode for reading
|
|
// requests from the client
|
|
//
|
|
|
|
LockUser();
|
|
if( QueryState() != UserStateDisconnected ) {
|
|
SetState( UserStateWaitingForUser);
|
|
}
|
|
UnlockUser();
|
|
|
|
if ( pvInitialRequest != NULL && cbInitialRequest > 0) {
|
|
|
|
//
|
|
// No need to issue a read, since we have the data required.
|
|
// Do a safe copy to the buffer.
|
|
//
|
|
|
|
CopyMemory( QueryReceiveBuffer(), pvInitialRequest,
|
|
min( cbInitialRequest, QueryReceiveBufferSize())
|
|
);
|
|
|
|
fReturn = TRUE;
|
|
|
|
} else {
|
|
|
|
fReturn = ReadCommand();
|
|
}
|
|
|
|
} else {
|
|
|
|
IF_DEBUG( ERROR) {
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
" SetsockOpt( OOB_INLINE) failed. Error = %lu\n",
|
|
WSAGetLastError()));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
DWORD dwError = (fReturn) ? NO_ERROR : GetLastError();
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" connection ( %08x)::StartupSession() returns %d."
|
|
" Error = %lu\n",
|
|
this, fReturn,
|
|
dwError));
|
|
|
|
if (fReturn) { SetLastError( dwError); }
|
|
}
|
|
|
|
return ( fReturn);
|
|
|
|
} // USER_DATA::StartupSession()
|
|
|
|
|
|
|
|
VOID
|
|
CheckAndProcessAbortOperation( IN LPUSER_DATA pUserData)
|
|
{
|
|
if ( TEST_UF( pUserData, OOB_ABORT)) {
|
|
|
|
//
|
|
// An abort was requested by client. So our processing
|
|
// has unwound and we are supposed to send some message
|
|
// to the client. ==> simulate processing ABOR command
|
|
// ABORT was not processed yet; so process now.
|
|
//
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"Executing simulated Abort for %08x\n",
|
|
pUserData));
|
|
|
|
FacIncrement( FacSimulatedAborts);
|
|
|
|
// To avoid thread races, check twice.
|
|
|
|
if ( TEST_UF( pUserData, OOB_ABORT)) {
|
|
|
|
//
|
|
// we need this stack variable (szAbort), so that
|
|
// ParseCommand() can freely modify the string!
|
|
CHAR szAbort[10];
|
|
|
|
CLEAR_UF( pUserData, OOB_ABORT);
|
|
|
|
CopyMemory( szAbort, "ABOR", sizeof("ABOR"));
|
|
ParseCommand( pUserData, szAbort);
|
|
}
|
|
}
|
|
|
|
return;
|
|
|
|
} // CheckAndProcessAbortOperation()
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::ParseAndProcessRequest(IN DWORD cchRequest)
|
|
/*++
|
|
This function parses the incoming request from client, identifies the
|
|
command to execute and executes the same.
|
|
Before parsing, the input is pre-processed to remove any of telnet commands
|
|
or OOB_inlined data.
|
|
|
|
Arguments:
|
|
cchRequest count of characters of request received.
|
|
|
|
--*/
|
|
{
|
|
BOOL fLineEnded = FALSE;
|
|
DWORD cchRequestRecvd = 0;
|
|
CHAR szCommandLine[ MAX_COMMAND_LENGTH + 1];
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( this))
|
|
{
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
IF_DEBUG( CLIENT)
|
|
{
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"UserData(%08x)::ParseAndProcessRequest( %d chars)\n",
|
|
this, cchRequest));
|
|
}
|
|
|
|
|
|
//
|
|
// Fast-path if we're re-processing this command, which happens in PASV mode
|
|
//
|
|
if ( QueryInFakeIOCompletion() )
|
|
{
|
|
goto FastPathLabel;
|
|
}
|
|
|
|
|
|
if ( cchRequest > 0)
|
|
{
|
|
// We have a valid request. Process it
|
|
|
|
// Update last access time
|
|
m_TimeAtLastAccess = GetCurrentTimeInSeconds();
|
|
|
|
m_pInstance->QueryStatsObj()->UpdateTotalBytesReceived(
|
|
cchRequest*sizeof(CHAR));
|
|
|
|
if ( m_cchPartialReqRecvd + cchRequest >= MAX_COMMAND_LENGTH)
|
|
{
|
|
#if DBG
|
|
CHAR szCmdFailed[600];
|
|
DBG_REQUIRE( _snprintf( szCmdFailed, sizeof( szCmdFailed ),
|
|
" Command is too long: Partial=%d bytes. Now=%d \n"
|
|
" UserDb(%08p) = %s from Host: %s\n",
|
|
m_cchPartialReqRecvd, cchRequest,
|
|
this, QueryUserName(), QueryClientHostName()) > 0);
|
|
|
|
DBGPRINTF((DBG_CONTEXT, szCmdFailed));
|
|
#endif
|
|
|
|
DisconnectUserWithError( ERROR_BUSY);
|
|
|
|
return ( TRUE); // we are done with this connection.
|
|
}
|
|
|
|
CopyMemory(szCommandLine, m_recvBuffer,
|
|
m_cchPartialReqRecvd + cchRequest);
|
|
szCommandLine[m_cchPartialReqRecvd + cchRequest] = '\0';
|
|
|
|
if ( !::FilterTelnetCommands(szCommandLine,
|
|
m_cchPartialReqRecvd + cchRequest,
|
|
&fLineEnded, &cchRequestRecvd))
|
|
{
|
|
|
|
if ( TEST_UF( this, TRANSFER))
|
|
{
|
|
|
|
//
|
|
// I am in data transfer mode. Some other thread is sending
|
|
// data for this client. Just post a OOB_DATA and OOB_ABORT
|
|
// OOB_DATA will cause the call-stack of other thread to unwind
|
|
// and get out of the command.
|
|
// Then check if any async transfer was occuring. If so
|
|
// process abort with disconnect now.
|
|
//
|
|
|
|
SET_UF_BITS( this, (UF_OOB_DATA | UF_OOB_ABORT));
|
|
|
|
if ( TEST_UF( this, ASYNC_TRANSFER))
|
|
{
|
|
|
|
//
|
|
// An async transfer is occuring. Stop it
|
|
//
|
|
DestroyDataConnection( ERROR_OPERATION_ABORTED);
|
|
|
|
CheckAndProcessAbortOperation( this);
|
|
}
|
|
|
|
# ifdef CHECK_DBG
|
|
|
|
Print( " OOB_ABORT ");
|
|
|
|
# endif // CHECK_DBG
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"[%08x]Set up the implied ABORT command\n",
|
|
this));
|
|
}
|
|
|
|
IF_DEBUG( COMMANDS) {
|
|
|
|
DBGPRINTF((DBG_CONTEXT, " ***** [%08x] OOB_ABORT Set \n",
|
|
this));
|
|
}
|
|
|
|
// Ignore the rest of the commands that may have come in.
|
|
}
|
|
else
|
|
{
|
|
|
|
//
|
|
// Since no command is getting processed.
|
|
// atleast process the abort command, otherwise clients hang.
|
|
//
|
|
|
|
//
|
|
// we need this stack variable (szAbort), so that
|
|
// ParseCommand() can freely modify the string!
|
|
CHAR szAbort[10];
|
|
|
|
CopyMemory( szAbort, "ABOR", sizeof("ABOR"));
|
|
ParseCommand( this, szAbort);
|
|
CLEAR_UF( this, OOB_ABORT); // clear the abort flag!
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
|
|
if ( TEST_UF( this, TRANSFER))
|
|
{
|
|
|
|
//
|
|
// we are transferring data, sorry no more commands accepted.
|
|
// This could hang clients. Hey! they asked for it :( NYI
|
|
//
|
|
|
|
// Do nothing
|
|
IF_DEBUG( COMMANDS) {
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"***** [%08x] Received Request %s during"
|
|
" transfer in progress\n",
|
|
this, szCommandLine));
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Let ParseCommand do the dirty work.
|
|
//
|
|
|
|
|
|
// Remember the count of partial bytes received.
|
|
m_cchPartialReqRecvd = cchRequestRecvd;
|
|
|
|
if ( !fLineEnded)
|
|
{
|
|
|
|
// In case if command was long enough to fill all buffer but
|
|
// we haven't found new line simply tell to user about the error
|
|
// and disconnect. Some ftp clients will not see that msg, becuase
|
|
// connection was disconnected, but thats a bug in client code
|
|
|
|
if ( m_cchPartialReqRecvd >= MAX_COMMAND_LENGTH - 1)
|
|
{
|
|
ReplyToUser( this,
|
|
REPLY_UNRECOGNIZED_COMMAND,
|
|
PSZ_COMMAND_TOO_LONG);
|
|
DisconnectUserWithError( ERROR_BUSY );
|
|
|
|
return ( TRUE); // we are done with this connection.
|
|
}
|
|
|
|
|
|
//
|
|
// Complete line is not received. Continue reading
|
|
// the requests, till we receive the complete request
|
|
//
|
|
|
|
}
|
|
else
|
|
{
|
|
|
|
StartProcessingTimer();
|
|
|
|
//
|
|
// set the partial received byte count to zero.
|
|
// we will not use this value till next incomplete request
|
|
//
|
|
|
|
m_cchPartialReqRecvd = 0;
|
|
|
|
FastPathLabel:
|
|
ParseCommand( this, ( QueryInFakeIOCompletion() ? QueryCmdString() :
|
|
szCommandLine ) );
|
|
|
|
CheckAndProcessAbortOperation( this);
|
|
|
|
} // if TRANSFER is not there...
|
|
|
|
} //Parse if complete
|
|
|
|
} // if FilterTelnetCommands()
|
|
}
|
|
else
|
|
{
|
|
// if (cchRequest <= 0)
|
|
|
|
SET_UF( this, CONTROL_ZERO);
|
|
|
|
//
|
|
// after a quit a client is expected to wait for quit message from
|
|
// the server. if the client prematurely closes connection, then
|
|
// the server receives it as a receive with zero byte read.
|
|
// since, we should not be having outstanding read at this time,
|
|
// atq should not be calling us. On the contrary we are getting
|
|
// called by ATQ. Let us track this down.
|
|
//
|
|
|
|
if ( !TEST_UF( this, CONTROL_QUIT))
|
|
{
|
|
DisconnectUserWithError( NO_ERROR);
|
|
}
|
|
else
|
|
{
|
|
|
|
// Quit message is received and then ZeroBytes Received!!
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
" (%08x)::ZeroBytes recvd after QUIT message!!."
|
|
" State = %d(%x), Ref = %d\n",
|
|
this,
|
|
QueryState(), Flags,
|
|
QueryReference()
|
|
));
|
|
// Do nothing. Since Quit will take care of cleanup
|
|
return (TRUE);
|
|
}
|
|
}
|
|
|
|
//
|
|
// If the connection is not yet disconnected, submit a read command.
|
|
// else return that everything is fine (someone had disconnected it).
|
|
//
|
|
|
|
return ( IsDisconnected() ? TRUE : ReadCommand());
|
|
} // USER_DATA::ParseAndProcessRequest()
|
|
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::ReadCommand( VOID)
|
|
{
|
|
BOOL fReturn = TRUE;
|
|
|
|
DBG_CODE(
|
|
if ( !IS_VALID_USER_DATA( this)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
);
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
if ( TEST_UF( this, CONTROL_TIMEDOUT) || IsDisconnected()) {
|
|
|
|
SetLastError( ERROR_SEM_TIMEOUT);
|
|
return (FALSE);
|
|
}
|
|
|
|
//
|
|
// Submit a read on control socket only if there is none pending!
|
|
// Otherwise, behave in idempotent manner.
|
|
//
|
|
|
|
if ( !TEST_UF( this, CONTROL_READ)) {
|
|
|
|
Reference(); // since we are going to set up async read.
|
|
|
|
InterlockedIncrement( &m_nControlRead);
|
|
|
|
DBG_ASSERT( m_nControlRead <= 1);
|
|
|
|
SET_UF( this, CONTROL_READ); // a read will be pending
|
|
|
|
if ( !m_AioControlConnection.ReadFile(QueryReceiveBuffer(),
|
|
QueryReceiveBufferSize())
|
|
) {
|
|
|
|
CLEAR_UF( this, CONTROL_READ); // since read failed.
|
|
|
|
DBG_REQUIRE( DeReference() > 0);
|
|
InterlockedDecrement( &m_nControlRead);
|
|
|
|
DWORD dwError = GetLastError();
|
|
|
|
IF_DEBUG( ERROR) {
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" User( %08x)::ReadCommand() failed. Ref = %d."
|
|
" Error = %d\n",
|
|
this, QueryReference(), dwError));
|
|
}
|
|
|
|
SetLastError( dwError);
|
|
fReturn = FALSE;
|
|
}
|
|
|
|
}
|
|
|
|
return ( fReturn);
|
|
} // USER_DATA::ReadCommand()
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::DisconnectUserWithError(IN DWORD dwError,
|
|
IN BOOL fNextMsg OPTIONAL)
|
|
/*++
|
|
This function disconnects a user with the error code provided.
|
|
It closes down the control connection by stopping ASYNC_IO.
|
|
If the fNextMsg is not set, then it also decrements the reference count
|
|
for the user data object, to be freed soon.
|
|
|
|
--*/
|
|
{
|
|
CHAR szBuffer[120];
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( this)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
this));
|
|
Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
IF_DEBUG ( CLIENT) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" USER_DATA( %08x)::DisconnectUserWithError( %lu, %d)."
|
|
" RefCount = %d\n",
|
|
this, dwError, fNextMsg, QueryReference()));
|
|
}
|
|
|
|
|
|
if (!fNextMsg) {
|
|
RemoveActiveReference();
|
|
}
|
|
|
|
LockUser();
|
|
|
|
if ( QueryState() == UserStateDisconnected) {
|
|
|
|
//
|
|
// It is already in disconnected state. Do nothing for disconnect.
|
|
//
|
|
|
|
UnlockUser();
|
|
|
|
} else {
|
|
|
|
SetState( UserStateDisconnected );
|
|
UnlockUser();
|
|
|
|
//
|
|
// terminate outstanding asynchronous AD IO queries for root directory
|
|
//
|
|
|
|
if ( m_pAdioReq != NULL) {
|
|
m_pAdioReq->EndRequest();
|
|
m_pAdioReq = NULL;
|
|
}
|
|
|
|
if( dwError == ERROR_SEM_TIMEOUT) {
|
|
|
|
const CHAR * apszSubStrings[3];
|
|
|
|
IF_DEBUG( CLIENT )
|
|
{
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"client (%08x) timed-out\n", this ));
|
|
}
|
|
|
|
DBG_REQUIRE( _snprintf( szBuffer, sizeof( szBuffer ),
|
|
"%lu", m_pInstance->QueryConnectionTimeout() ) > 0);
|
|
|
|
apszSubStrings[0] = QueryUserName();
|
|
apszSubStrings[1] = inet_ntoa( HostIpAddress );
|
|
apszSubStrings[2] = szBuffer;
|
|
|
|
g_pInetSvc->LogEvent( FTPD_EVENT_CLIENT_TIMEOUT,
|
|
3,
|
|
apszSubStrings,
|
|
0 );
|
|
|
|
ReplyToUser(this,
|
|
REPLY_SERVICE_NOT_AVAILABLE,
|
|
"Timeout (%lu seconds): closing control connection.",
|
|
m_pInstance->QueryConnectionTimeout() );
|
|
}
|
|
|
|
if ( dwError != NO_ERROR) {
|
|
|
|
# ifdef CHECK_DBG
|
|
DBG_REQUIRE( _snprintf( szBuffer, sizeof( szBuffer ),
|
|
" Control Socket Error=%u ", dwError) > 0);
|
|
Print( szBuffer);
|
|
# endif // CHECK_DBG
|
|
|
|
if( dwError != ERROR_SEM_TIMEOUT ) {
|
|
SetLastReplyCode( REPLY_TRANSFER_ABORTED );
|
|
}
|
|
|
|
// Produce a log record indicating the cause for failure.
|
|
WriteLogRecord( PSZ_CONNECTION_CLOSED_VERB, "", dwError);
|
|
}
|
|
|
|
//
|
|
// Force close the connection's sockets. This will cause the
|
|
// thread to awaken from any blocked socket operation. It
|
|
// is the destructor's responsibility to do any further cleanup.
|
|
// (such as calling UserDereference()).
|
|
//
|
|
|
|
CloseSockets(dwError != NO_ERROR);
|
|
}
|
|
|
|
return ( TRUE);
|
|
|
|
} // USER_DATA::DisconnectUserWithError()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static BOOL
|
|
DisconnectUserWorker( IN LPUSER_DATA pUserData, IN LPVOID pContext)
|
|
/*++
|
|
This disconnects (logically) a user connection, by resetting the
|
|
control connection and stopping IO. Later on the blown away socket
|
|
will cause an ATQ relinquish to occur to blow away of this connection.
|
|
|
|
Arguments:
|
|
pUserData pointer to User data object for connection to be disconnected.
|
|
pContext pointer to context information
|
|
( in this case to DWORD containing error code indicating reasong for
|
|
disconnect).
|
|
|
|
Returns:
|
|
TRUE on success and FALSE if there is any failure.
|
|
|
|
--*/
|
|
{
|
|
DWORD dwError;
|
|
BOOL retVal;
|
|
DBG_ASSERT( pContext != NULL && pUserData != NULL);
|
|
DBG_ASSERT( IS_VALID_USER_DATA( pUserData ) );
|
|
|
|
|
|
dwError = *(LPDWORD ) pContext;
|
|
|
|
|
|
retVal = pUserData->DisconnectUserWithError( dwError, TRUE);
|
|
|
|
// fix for bug 268175 : if we disconnected user we need to do normal cleanup
|
|
// for that connection
|
|
|
|
// this check is not very necessary but I leave it for future
|
|
// DisconnectUserWithError always returns TRUE
|
|
|
|
if (retVal)
|
|
{
|
|
DereferenceUserDataAndKill(pUserData);
|
|
}
|
|
|
|
|
|
return retVal;
|
|
} // DisconnectUserWorker()
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
DisconnectUser( IN DWORD UserId, FTP_SERVER_INSTANCE *pInstance )
|
|
/*++
|
|
This function disconnects a specified user identified using the UserId.
|
|
If UserId specified == 0, then all the users will be disconnected.
|
|
|
|
Arguments:
|
|
UserId user id for the connection to be disconnected.
|
|
|
|
Returns:
|
|
TRUE if atleast one of the connections is disconnected.
|
|
FALSE if no user connetion found.
|
|
|
|
History:
|
|
06-April-1995 Created.
|
|
--*/
|
|
{
|
|
BOOL fFound;
|
|
DWORD dwContext = ERROR_SERVER_DISABLED;
|
|
|
|
pInstance->Reference();
|
|
pInstance->LockConnectionsList();
|
|
|
|
fFound = ( pInstance->
|
|
EnumerateConnection( DisconnectUserWorker,
|
|
(LPVOID ) &dwContext,
|
|
UserId));
|
|
|
|
pInstance->UnlockConnectionsList();
|
|
pInstance->Dereference();
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
DWORD dwError = (fFound) ? NO_ERROR: GetLastError();
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" DisconnectUser( %d) returns %d. Error = %lu\n",
|
|
UserId, fFound, dwError));
|
|
|
|
if (fFound) { SetLastError( dwError); }
|
|
}
|
|
|
|
return ( fFound);
|
|
} // DisconnectUser()
|
|
|
|
|
|
|
|
|
|
|
|
static BOOL
|
|
DisconnectUserWithNoAccessWorker( IN LPUSER_DATA pUserData,
|
|
IN LPVOID pContext)
|
|
/*++
|
|
This disconnects (logically) a user connection with no access.
|
|
This occurs by resetting the control connection and stopping IO.
|
|
Later on the blown away thread
|
|
will cause an ATQ relinquish to occur to blow away of this connection.
|
|
|
|
Arguments:
|
|
pUserData pointer to User data object for connection to be disconnected.
|
|
pContext pointer to context information
|
|
( in this case to DWORD containing error code indicating reasong for
|
|
disconnect).
|
|
|
|
Returns:
|
|
TRUE on success and FALSE if there is any failure.
|
|
|
|
--*/
|
|
{
|
|
BOOL fSuccess = TRUE;
|
|
DBG_ASSERT( pUserData != NULL);
|
|
|
|
// Ignode the pContext information.
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( pUserData ) );
|
|
|
|
//
|
|
// We're only interested in connected users.
|
|
//
|
|
|
|
if( pUserData->IsLoggedOn()) {
|
|
|
|
//
|
|
// If this user no longer has access to their
|
|
// current directory, blow them away.
|
|
//
|
|
|
|
if( !pUserData->VirtualPathAccessCheck(AccessTypeRead )) {
|
|
|
|
const CHAR * apszSubStrings[2];
|
|
|
|
IF_DEBUG( SECURITY ) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"User %s (%lu) @ %08lX retroactively"
|
|
" denied access to %s\n",
|
|
pUserData->QueryUserName(),
|
|
pUserData->QueryId(),
|
|
pUserData,
|
|
pUserData->QueryCurrentDirectory().QueryStr() ));
|
|
}
|
|
|
|
|
|
fSuccess = ( pUserData->
|
|
DisconnectUserWithError(ERROR_ACCESS_DENIED,
|
|
TRUE)
|
|
);
|
|
|
|
//
|
|
// Log an event to tell the admin what happened.
|
|
//
|
|
|
|
apszSubStrings[0] = pUserData->QueryUserName();
|
|
apszSubStrings[1] = pUserData->QueryCurrentDirectory().QueryStr();
|
|
|
|
g_pInetSvc->LogEvent( FTPD_EVENT_RETRO_ACCESS_DENIED,
|
|
2,
|
|
apszSubStrings,
|
|
0 );
|
|
} // no access
|
|
|
|
} // logged on user
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
DWORD dwError = (fSuccess) ? NO_ERROR: GetLastError();
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" DisconnectUsersWithNoAccessWorker( %d) returns %d."
|
|
" Error = %lu\n",
|
|
pUserData->QueryId(), fSuccess,
|
|
dwError)
|
|
);
|
|
|
|
if (fSuccess) { SetLastError( dwError); }
|
|
}
|
|
|
|
return ( fSuccess);
|
|
} // DisconnectUserWithNoAccessWorker()
|
|
|
|
|
|
|
|
VOID
|
|
DisconnectUsersWithNoAccess(FTP_SERVER_INSTANCE *pInstance )
|
|
/*++
|
|
This function disconnects all users who do not have read access to
|
|
their current directory. This is typically called when the access masks
|
|
have been changed.
|
|
|
|
Arguments:
|
|
None
|
|
|
|
Returns:
|
|
None.
|
|
--*/
|
|
{
|
|
BOOL fFound;
|
|
DWORD dwContext = ERROR_ACCESS_DENIED;
|
|
|
|
pInstance->Reference();
|
|
pInstance->LockConnectionsList();
|
|
|
|
fFound = ( pInstance->
|
|
EnumerateConnection( DisconnectUserWithNoAccessWorker,
|
|
(LPVOID ) &dwContext,
|
|
0));
|
|
|
|
pInstance->UnlockConnectionsList();
|
|
pInstance->Dereference();
|
|
|
|
IF_DEBUG( CLIENT) {
|
|
|
|
DWORD dwError = (fFound) ? NO_ERROR: GetLastError();
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" DisconnectUsersWithNoAccess() returns %d."
|
|
" Error = %lu\n",
|
|
fFound, dwError)
|
|
);
|
|
|
|
if (fFound) { SetLastError( dwError); }
|
|
}
|
|
|
|
|
|
} // DisconnectUsersWithNoAccess
|
|
|
|
|
|
|
|
|
|
/*++
|
|
The following structure UserEnumBuffer is required to carry the context
|
|
information for enumerating the users currently connected.
|
|
It contains a pointer to array of USER_INFO structures which contain the
|
|
specific information for the user. The user name is stored in the buffer
|
|
from the end ( so that null terminated strings are formed back to back.
|
|
This permits efficient storage of variable length strings.
|
|
|
|
The member fResult is used to carry forward the partial result of
|
|
success/failure from one user to another ( since the enumeration has
|
|
to walk through all the elements to find out all user information).
|
|
|
|
|
|
History: MuraliK ( 12-April-1995)
|
|
|
|
--*/
|
|
struct USER_ENUM_BUFFER {
|
|
|
|
DWORD cbSize; // pointer to dword containing size of
|
|
IIS_USER_INFO_1 * pUserInfo; // pointer to start of array of USER_INFO
|
|
DWORD cbRequired; // incremental count of bytes required.
|
|
DWORD nEntry; // number of current entry ( index into pUserInfo)
|
|
DWORD dwCurrentTime; // current time
|
|
WCHAR * pszNext; // pointer to next string location.
|
|
BOOL fResult; // boolean flag accumulating partial results
|
|
};
|
|
|
|
typedef USER_ENUM_BUFFER * PUSER_ENUM_BUFFER;
|
|
|
|
|
|
BOOL
|
|
EnumerateUserInBufferWorker( IN LPUSER_DATA pUserData,
|
|
IN LPVOID pContext)
|
|
{
|
|
# ifdef CHECK_DBG
|
|
CHAR szBuffer[400];
|
|
# endif // CHECK_DBG
|
|
|
|
PUSER_ENUM_BUFFER pUserEnumBuffer = (PUSER_ENUM_BUFFER ) pContext;
|
|
DWORD tConnect;
|
|
DWORD cbUserName;
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( pUserData ) );
|
|
|
|
//
|
|
// We're only interested in connected users.
|
|
//
|
|
|
|
if( pUserData->IsDisconnected()) {
|
|
|
|
return ( TRUE);
|
|
}
|
|
|
|
//
|
|
// Determine required buffer size for current user.
|
|
//
|
|
|
|
cbUserName = ( strlen( pUserData->QueryUserName() ) + 1 ) * sizeof(WCHAR);
|
|
pUserEnumBuffer->cbRequired += sizeof(IIS_USER_INFO_1);
|
|
|
|
//
|
|
// If there's room for the user data, store it.
|
|
//
|
|
|
|
tConnect = ( pUserEnumBuffer->dwCurrentTime -
|
|
pUserData->QueryTimeAtConnection());
|
|
|
|
if( pUserEnumBuffer->fResult &&
|
|
( pUserEnumBuffer->cbRequired <= pUserEnumBuffer->cbSize)
|
|
) {
|
|
|
|
LPIIS_USER_INFO_1 pUserInfo =
|
|
&pUserEnumBuffer->pUserInfo[ pUserEnumBuffer->nEntry];
|
|
|
|
pUserInfo->idUser = pUserData->QueryId();
|
|
pUserInfo->pszUser = (WCHAR *)MIDL_user_allocate( cbUserName );
|
|
|
|
if( pUserInfo->pszUser ) {
|
|
|
|
pUserInfo->fAnonymous = ( pUserData->Flags & UF_ANONYMOUS ) != 0;
|
|
pUserInfo->inetHost = (DWORD)pUserData->HostIpAddress.s_addr;
|
|
pUserInfo->tConnect = tConnect;
|
|
|
|
if( !MultiByteToWideChar( CP_OEMCP,
|
|
0,
|
|
pUserData->QueryUserName(),
|
|
-1,
|
|
pUserInfo->pszUser,
|
|
(int)cbUserName )
|
|
) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"MultiByteToWideChar failed???\n" ));
|
|
|
|
pUserEnumBuffer->fResult = ( pUserEnumBuffer->fResult && FALSE);
|
|
} else {
|
|
pUserEnumBuffer->nEntry++;
|
|
}
|
|
}
|
|
else {
|
|
|
|
//
|
|
// Unable to allocate memory
|
|
//
|
|
pUserEnumBuffer->fResult = ( pUserEnumBuffer->fResult && FALSE);
|
|
}
|
|
|
|
} else {
|
|
|
|
pUserEnumBuffer->fResult = ( pUserEnumBuffer->fResult && FALSE);
|
|
}
|
|
|
|
# ifdef CHECK_DBG
|
|
|
|
DBG_REQUIRE( _snprintf( szBuffer, sizeof( szBuffer ),
|
|
" Enum tLastAction=%u; tConnect=%u. " ,
|
|
( pUserEnumBuffer->dwCurrentTime -
|
|
pUserData->QueryTimeAtLastAccess()),
|
|
tConnect
|
|
) > 0);
|
|
|
|
pUserData->Print( szBuffer);
|
|
|
|
# endif // CHECK_DBG
|
|
|
|
return ( TRUE);
|
|
} // EnumerateUserInBufferWorker()
|
|
|
|
|
|
|
|
BOOL
|
|
EnumerateUsers(
|
|
PCHAR pBuffer,
|
|
PDWORD pcbBuffer,
|
|
PDWORD nRead,
|
|
FTP_SERVER_INSTANCE *pInstance
|
|
)
|
|
/*++
|
|
Enumerates the current active users into the specified buffer.
|
|
|
|
Arguments:
|
|
pvEnum pointer to enumeration buffer which will receive the number of
|
|
entries and the user information.
|
|
pcbBuffer pointer to count of bytes. On entry this contains the size in
|
|
bytes of the enumeration buffer. It receives the count
|
|
of bytes for enumerating all the users.
|
|
nRead - pointer to a DWORD to return the number of user entries filled.
|
|
|
|
Returns:
|
|
TRUE if enumeration is successful ( all connected users accounted for)
|
|
FALSE otherwise
|
|
|
|
--*/
|
|
{
|
|
USER_ENUM_BUFFER userEnumBuffer;
|
|
BOOL fSuccess;
|
|
|
|
DBG_ASSERT( pcbBuffer != NULL );
|
|
|
|
IF_DEBUG( USER_DATABASE) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Entering EnumerateUsers( %08x, %08x[%d]).\n",
|
|
pBuffer, pcbBuffer, *pcbBuffer));
|
|
}
|
|
|
|
//
|
|
// Setup the data in user enumeration buffer.
|
|
//
|
|
|
|
userEnumBuffer.cbSize = *pcbBuffer;
|
|
userEnumBuffer.cbRequired = 0;
|
|
userEnumBuffer.pUserInfo = (LPIIS_USER_INFO_1)pBuffer;
|
|
userEnumBuffer.nEntry = 0;
|
|
userEnumBuffer.dwCurrentTime = GetCurrentTimeInSeconds();
|
|
userEnumBuffer.fResult = TRUE;
|
|
|
|
//
|
|
// CODEWORK
|
|
// This field is obsolete it now points to the extra CONN_LEEWAY
|
|
// buffer.
|
|
//
|
|
userEnumBuffer.pszNext = ((WCHAR *)( pBuffer + *pcbBuffer));
|
|
|
|
|
|
//
|
|
// Scan the users and get the information required.
|
|
//
|
|
|
|
pInstance->Reference();
|
|
pInstance->LockConnectionsList();
|
|
|
|
fSuccess = (pInstance->
|
|
EnumerateConnection( EnumerateUserInBufferWorker,
|
|
(LPVOID ) &userEnumBuffer,
|
|
0));
|
|
|
|
pInstance->UnlockConnectionsList();
|
|
pInstance->Dereference();
|
|
|
|
//
|
|
// Update enum buffer header.
|
|
//
|
|
|
|
*nRead = userEnumBuffer.nEntry;
|
|
*pcbBuffer = userEnumBuffer.cbRequired;
|
|
|
|
IF_DEBUG( USER_DATABASE) {
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
" Leaving EnumerateUsers() with %d."
|
|
" Entries read =%d. BufferSize required = %d\n",
|
|
userEnumBuffer.fResult,
|
|
userEnumBuffer.nEntry, userEnumBuffer.cbRequired));
|
|
}
|
|
|
|
return ( userEnumBuffer.fResult);
|
|
|
|
} // EnumerateUsers
|
|
|
|
|
|
|
|
|
|
SOCKERR
|
|
USER_DATA::SendMultilineMessage(
|
|
IN UINT nReplyCode,
|
|
IN LPCSTR pszzMessage,
|
|
IN BOOL fIsFirst,
|
|
IN BOOL fIsLast)
|
|
/*++
|
|
Sends a multiline message to the control socket of the client.
|
|
|
|
Arguments:
|
|
nReplyCode the reply code to use for the first line of the multi-line
|
|
message.
|
|
pszzMessage pointer to double null terminated sequence of strings
|
|
containing the message to be sent.
|
|
fIsFirst flag to indicate we are starting the multiline reply. if FALSE,
|
|
don't print the code for the first line, as it was already emmited elsewhere
|
|
fIsLast flag to indicate we are finishing the multiline reply. if FALSE,
|
|
don't print the code for the first line, as it was already emmited elsewhere
|
|
|
|
If the message is empty, we do not print anything. If there is only one line, then if
|
|
fIsLast is TRUE, we only print the terminating line, otherwise we do print the openning
|
|
line if fIsFirst is TRUE.
|
|
|
|
Returns:
|
|
SOCKERR - 0 if successful, !0 if not.
|
|
|
|
History:
|
|
MuraliK 12-April-1995
|
|
--*/
|
|
{
|
|
SOCKERR serr = 0;
|
|
LPCSTR pszMsg, pszNext;
|
|
|
|
//
|
|
// return if there is nothing to send
|
|
//
|
|
|
|
if ( pszzMessage == NULL || *pszzMessage == '\0') {
|
|
return serr;
|
|
}
|
|
|
|
|
|
for ( pszMsg = pszzMessage; serr == 0 && *pszMsg != '\0'; pszMsg = pszNext) {
|
|
|
|
//
|
|
// find next message so that we can check of pszMsg is the last line
|
|
//
|
|
|
|
pszNext = pszMsg + strlen( pszMsg) + 1;
|
|
|
|
if( fIsLast && *pszNext == '\0' ) {
|
|
//
|
|
// This is globally the last line. Print it pefixed with the reply code.
|
|
//
|
|
serr = SockPrintf2(this, QueryControlSocket(),
|
|
"%u %s",
|
|
nReplyCode,
|
|
pszMsg);
|
|
|
|
} else if( fIsFirst ) {
|
|
//
|
|
// this is globally the first line of reply, and it is not globally the last one.
|
|
// print it with '-'.
|
|
//
|
|
serr = SockPrintf2(this, QueryControlSocket(),
|
|
"%u-%s",
|
|
nReplyCode,
|
|
pszMsg);
|
|
|
|
fIsFirst = FALSE;
|
|
} else {
|
|
//
|
|
// this is either an intermediate line, or the last line in this batch (but
|
|
// not globally), so print it idented without the reply code.
|
|
//
|
|
serr = SockPrintf2(this, QueryControlSocket(),
|
|
" %s",
|
|
pszMsg);
|
|
}
|
|
} // for
|
|
|
|
return ( serr);
|
|
|
|
} // USER_DATA::SendMultilineMessge()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SOCKERR
|
|
USER_DATA::SendDirectoryAnnotation( IN UINT ReplyCode, IN BOOL fIsFirst)
|
|
/*++
|
|
SYNOPSIS: Tries to open the FTPD_ANNOTATION_FILE (~~ftpsvc~~.ckm)
|
|
file in the user's current directory. If it can be
|
|
opened, it is sent to the user over the command socket
|
|
as a multi-line reply.
|
|
|
|
ENTRY:
|
|
ReplyCode - The reply code to send as the first line
|
|
of this multi-line reply.
|
|
|
|
fIsFirst - flag to indicate if this is the first line in the multi-line
|
|
reply. If not, the ReplyCode is not shown
|
|
|
|
RETURNS: SOCKERR - 0 if successful, !0 if not.
|
|
|
|
HISTORY:
|
|
KeithMo 06-May-1993 Created.
|
|
MuraliK 12-Apr-1995 Made it to be part of USER_DATA
|
|
--*/
|
|
{
|
|
FILE * pfile;
|
|
SOCKERR serr = 0;
|
|
CHAR szLine[MAX_REPLY_LENGTH+1];
|
|
|
|
|
|
//
|
|
// Try to open the annotation file.
|
|
//
|
|
|
|
pfile = Virtual_fopen( this,
|
|
FTPD_ANNOTATION_FILE,
|
|
"r" );
|
|
|
|
if( pfile == NULL )
|
|
{
|
|
//
|
|
// File not found. Blow it off.
|
|
//
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// protection agians attack when CKM file islarge, somebody is downloading it
|
|
// slowly on many connections and uses all ATQ threads. Note that attack is still possible
|
|
// but much more difficult to achieve
|
|
|
|
AtqSetInfo( AtqIncMaxPoolThreads, 0);
|
|
|
|
//
|
|
// While there's more text in the file, blast
|
|
// it to the user.
|
|
//
|
|
|
|
while( fgets( szLine, MAX_REPLY_LENGTH, pfile ) != NULL )
|
|
{
|
|
CHAR * pszTmp = szLine + strlen(szLine) - 1;
|
|
|
|
//
|
|
// Remove any trailing CR/LFs in the string.
|
|
//
|
|
|
|
while( ( pszTmp >= szLine ) &&
|
|
( ( *pszTmp == '\n' ) || ( *pszTmp == '\r' ) ) )
|
|
{
|
|
*pszTmp-- = '\0';
|
|
}
|
|
|
|
//
|
|
// Ensure we send the proper prefix for the
|
|
// very *first* line of the file.
|
|
//
|
|
|
|
if( fIsFirst )
|
|
{
|
|
serr = SockPrintf2(this,
|
|
QueryControlSocket(),
|
|
"%u-%s",
|
|
ReplyCode,
|
|
szLine );
|
|
|
|
fIsFirst = FALSE;
|
|
}
|
|
else
|
|
{
|
|
serr = SockPrintf2(this,
|
|
QueryControlSocket(),
|
|
" %s",
|
|
szLine );
|
|
}
|
|
|
|
if( serr != 0 )
|
|
{
|
|
//
|
|
// Socket error sending file.
|
|
//
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
AtqSetInfo( AtqDecMaxPoolThreads, 0);
|
|
|
|
//
|
|
// Cleanup.
|
|
//
|
|
|
|
if ( 0 != fclose( pfile )) {
|
|
|
|
IF_DEBUG( ERROR) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"[%08x]::SendAnnotationFile() file close failed. "
|
|
" Error = %d\n",
|
|
this,
|
|
GetLastError()
|
|
));
|
|
}
|
|
}
|
|
|
|
return serr;
|
|
|
|
} // USER_DATA::SendDirectoryAnnotation()
|
|
|
|
|
|
|
|
|
|
SOCKERR
|
|
USER_DATA::SendErrorToClient(
|
|
IN LPCSTR pszPath,
|
|
IN DWORD dwError,
|
|
IN LPCSTR pszDefaultErrorMsg,
|
|
IN UINT nReplyCode
|
|
)
|
|
/*++
|
|
Send an error message indicating that the path is not found or
|
|
a particular error occured in a path.
|
|
|
|
Arguments:
|
|
sock socket to be used for synchronously sending message
|
|
pszPath pointer to path to be used.
|
|
dwError DWORD containing the error code, used for getting error text.
|
|
pszDefaultErrorMsg pointer to null-terminated string containing the
|
|
error message to be used if we can't alloc error text.
|
|
nReplyCode UINT containing the FTP reply code.
|
|
|
|
Returns:
|
|
SOCKERR. 0 if successful and !0 if failure.
|
|
--*/
|
|
{
|
|
BOOL fDelete = TRUE;
|
|
LPCSTR pszText;
|
|
APIERR serr;
|
|
|
|
DBG_ASSERT( pszPath != NULL);
|
|
pszText = AllocErrorText( dwError );
|
|
|
|
if( pszText == NULL ) {
|
|
|
|
pszText = pszDefaultErrorMsg;
|
|
fDelete = FALSE;
|
|
}
|
|
|
|
serr = ReplyToUser( this,
|
|
nReplyCode,
|
|
PSZ_FILE_ERROR,
|
|
pszPath,
|
|
pszText );
|
|
|
|
if( fDelete ) {
|
|
|
|
FreeErrorText( (char *) pszText );
|
|
}
|
|
|
|
return ( serr);
|
|
} // USER_DATA::SendErrorToClient()
|
|
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::FreeUserToken( VOID)
|
|
/*++
|
|
|
|
This function frees the user token if present already.
|
|
Otherwise does nothing.
|
|
--*/
|
|
{
|
|
BOOL fReturn = TRUE;
|
|
|
|
if( UserToken != NULL ) {
|
|
|
|
fReturn = TsDeleteUserToken( UserToken );
|
|
|
|
UserToken = NULL;
|
|
::RevertToSelf();
|
|
}
|
|
|
|
return ( fReturn);
|
|
} // USER_DATA::FreeUserToken()
|
|
|
|
|
|
APIERR
|
|
USER_DATA::CdToUsersHomeDirectory()
|
|
/*++
|
|
This function changes user's home directory.
|
|
First, a CD to the virtual root is attempted.
|
|
If this succeeds, a CD to pszUser is attempted.
|
|
If this fails, a CD to DEFAULT_SUB_DIRECTORY is attempted.
|
|
|
|
Returns:
|
|
APIERR. NO_ERROR on success.
|
|
--*/
|
|
{
|
|
APIERR err;
|
|
CHAR rgchRoot[MAX_PATH];
|
|
|
|
//
|
|
// Try the top-level home directory. If this fails, bag out.
|
|
// Set and try to change directory to symbolic root.
|
|
//
|
|
|
|
m_szCurrentDirectory.Reset(); // initially nothing.
|
|
|
|
m_pInstance->LockThisForRead();
|
|
DBG_ASSERT( strlen( m_pInstance->QueryRoot()) < MAX_PATH);
|
|
P_strncpy( rgchRoot, m_pInstance->QueryRoot(), MAX_PATH);
|
|
m_pInstance->UnlockThis();
|
|
|
|
err = VirtualChDir( this, rgchRoot); // change to default dir.
|
|
|
|
if ( (err == NO_ERROR) &&
|
|
(m_pInstance->QueryIsolationMode() == MD_USER_ISOLATION_NONE) ) {
|
|
|
|
//
|
|
// We successfully CD'd into the top-level home
|
|
// directory. Now see if we can CD into pszUser.
|
|
//
|
|
|
|
P_strncpy( rgchRoot, QueryRootDirectory().QueryStr(), MAX_PATH);
|
|
if ( !VirtualChDir( this, rgchRoot ) ) {
|
|
|
|
//
|
|
// Nope, try DEFAULT_SUB_DIRECTORY. If this fails, just
|
|
// hang-out at the top-level home directory.
|
|
//
|
|
|
|
P_strncpy( rgchRoot, PSZ_DEFAULT_SUB_DIRECTORY, MAX_PATH);
|
|
VirtualChDir( this, rgchRoot );
|
|
}
|
|
}
|
|
|
|
return ( err);
|
|
|
|
} // USER_DATA::CdToUsersHomeDirectory()
|
|
|
|
|
|
VOID
|
|
USER_DATA::SignalRootDirReady(
|
|
HANDLE hUserData,
|
|
DWORD dwResult)
|
|
/*++
|
|
|
|
Function that restarts processing the original command when a PASV data socket becomes
|
|
accept()'able [ie the client has made the connection]
|
|
|
|
Arguments:
|
|
pUserData - USER_DATA context attached to socket
|
|
|
|
Returns:
|
|
Nothing
|
|
--*/
|
|
{
|
|
LPUSER_DATA pUserData = (LPUSER_DATA)hUserData;
|
|
|
|
PATQ_CONTEXT pAtqContext = pUserData->QueryControlAio()->QueryAtqContext();
|
|
|
|
pUserData->SetRootDirStatus( dwResult );
|
|
|
|
pUserData->SetInFakeIOCompletion( TRUE );
|
|
|
|
pUserData->m_pAdioReq = NULL; // we no longer need this
|
|
|
|
//
|
|
// do a scary thing - fake an IO completion, to trigger re-processing of the FTP command
|
|
//
|
|
if ( !AtqPostCompletionStatus( pAtqContext,
|
|
strlen( pUserData->QueryCmdString() ) + 1 ) )
|
|
{
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"Failed to post fake completion status to deal with async ADIO : 0x%x\n",
|
|
GetLastError()));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
APIERR
|
|
USER_DATA::SetRootDirectory(
|
|
LPSTR pszCmd)
|
|
/*++
|
|
this function sets the home directory for the logging in user,
|
|
based on the current isolation mode. If we are in no-isolation mode,
|
|
the user name excluding domain name is used. In basic isolation mode <domain>\<user>
|
|
is used for domain users, and local\<user> for local domain users. The
|
|
anonymous user gets 'LocalUser\public' in the isolation mode, and pszAnonymousName in
|
|
the no-isolation mode. For AD isolatin mode, the home directory is
|
|
retrieved from the AD
|
|
|
|
Arguments:
|
|
pszCmd Pointer to command line. we need to preserve it for an asynchronous call
|
|
|
|
Returns:
|
|
NO_ERROR on success, WinError otherwise.
|
|
--*/
|
|
{
|
|
APIERR RetVal = NO_ERROR;
|
|
PSTR pszUser;
|
|
PSTR pszDomain;
|
|
BOOL fIsLocalUser;
|
|
CHAR szDomainAndUser[ UNLEN + DNLEN + 2 ];
|
|
|
|
//
|
|
// handle anonymous users first
|
|
//
|
|
|
|
if( TEST_UF( this, ANONYMOUS ) ) {
|
|
switch (m_pInstance->QueryIsolationMode()) {
|
|
|
|
case MD_USER_ISOLATION_NONE:
|
|
//
|
|
// Anonymous users are mapped to a static name directory.
|
|
//
|
|
|
|
m_strRootDir.Copy( '\\' );
|
|
|
|
m_strRootDir.Append( PSZ_ANONYMOUS_NAME);
|
|
|
|
break;
|
|
|
|
case MD_USER_ISOLATION_BASIC:
|
|
//
|
|
// Anonymous users are mapped to a static named directory
|
|
// under the local accounts directory.
|
|
//
|
|
|
|
m_strRootDir.Copy( '\\' );
|
|
|
|
m_strRootDir.Append( PSZ_LOCALUSER_DIR, sizeof( PSZ_LOCALUSER_DIR ) - 1 );
|
|
|
|
m_strRootDir.Append( PSZ_ANONYMOUS_DIR, sizeof( PSZ_ANONYMOUS_DIR ) - 1 );
|
|
|
|
break;
|
|
|
|
case MD_USER_ISOLATION_AD:
|
|
|
|
DBG_ASSERT( m_pInstance->QueryAdIo() != NULL );
|
|
|
|
RetVal = m_pInstance->QueryAnonymousHomeDir( m_strRootDir );
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
DBG_ASSERT( FALSE );
|
|
RetVal = ERROR_INTERNAL_ERROR;
|
|
}
|
|
|
|
goto ExitPoint;
|
|
}
|
|
|
|
//
|
|
// now, on to authenticated users. find the logged on user full name
|
|
//
|
|
if( !ImpersonateUser()) {
|
|
|
|
//
|
|
// Impersonation failure.
|
|
//
|
|
|
|
RetVal = ERROR_ACCESS_DENIED;
|
|
goto ExitPoint;
|
|
|
|
} else {
|
|
DWORD dwSize = sizeof(szDomainAndUser);
|
|
if (!GetUserNameEx(NameSamCompatible,
|
|
szDomainAndUser,
|
|
&dwSize)) {
|
|
|
|
RetVal = GetLastError();
|
|
}
|
|
|
|
|
|
RevertToSelf();
|
|
|
|
if (RetVal != ERROR_SUCCESS) {
|
|
goto ExitPoint;
|
|
}
|
|
}
|
|
|
|
|
|
if (!ParseUserName(
|
|
szDomainAndUser,
|
|
&pszUser,
|
|
&pszDomain,
|
|
m_pInstance->QueryLocalHostName(),
|
|
&fIsLocalUser)) {
|
|
|
|
RetVal = ERROR_INVALID_PARAMETER;
|
|
goto ExitPoint;
|
|
}
|
|
|
|
switch (m_pInstance->QueryIsolationMode()) {
|
|
|
|
case MD_USER_ISOLATION_NONE:
|
|
//
|
|
// in the MD_USER_ISOLATION_NONE mode, compatible with pre 6.0 functionality, we set
|
|
// the root directory to the user name, excluding the domain (if provided).
|
|
//
|
|
|
|
m_strRootDir.Copy( '\\' );
|
|
|
|
m_strRootDir.Append( pszUser );
|
|
|
|
break;
|
|
|
|
case MD_USER_ISOLATION_BASIC:
|
|
//
|
|
// in the MD_USER_ISOLATION_BASIC mode, users logging in without specifying a domain
|
|
// are mapped to their user name under a static local directory name, while users
|
|
// logging in with a domain name are mapped to their user name directory under the
|
|
// domain name directory.
|
|
//
|
|
|
|
m_strRootDir.Copy( '\\' );
|
|
|
|
if ( fIsLocalUser ) {
|
|
//
|
|
// local user
|
|
//
|
|
m_strRootDir.Append( PSZ_LOCALUSER_DIR, sizeof( PSZ_LOCALUSER_DIR ) - 1 );
|
|
|
|
m_strRootDir.Append( pszUser );
|
|
|
|
} else {
|
|
|
|
//
|
|
// domain user
|
|
//
|
|
m_strRootDir.Append( pszDomain );
|
|
m_strRootDir.Append( "\\", 1 );
|
|
m_strRootDir.Append( pszUser );
|
|
}
|
|
|
|
break;
|
|
|
|
case MD_USER_ISOLATION_AD:
|
|
|
|
DBG_ASSERT( m_pInstance->QueryAdIo() != NULL );
|
|
|
|
if ( fIsLocalUser) {
|
|
//
|
|
// we don't allow local user in this isolation mode
|
|
//
|
|
|
|
RetVal = ERROR_INVALID_PARAMETER;
|
|
break;
|
|
}
|
|
|
|
//
|
|
// make a async call to retrieve the properties from AD
|
|
//
|
|
|
|
Reference(); // ++ reference before issuing an async call
|
|
|
|
RetVal = m_pInstance->QueryUserHomeDir(
|
|
pszUser,
|
|
pszDomain,
|
|
&m_strRootDir,
|
|
&m_pAdioReq,
|
|
&SignalRootDirReady,
|
|
(HANDLE)this);
|
|
|
|
if (RetVal != ERROR_IO_PENDING) {
|
|
//
|
|
// remove the reference as the async call failed
|
|
//
|
|
|
|
DBG_REQUIRE( DeReference() > 0 );
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
DBG_ASSERT( FALSE );
|
|
RetVal = ERROR_INTERNAL_ERROR;
|
|
}
|
|
|
|
ExitPoint:
|
|
SetRootDirStatus( RetVal );
|
|
|
|
return RetVal;
|
|
}
|
|
|
|
|
|
|
|
APIERR
|
|
USER_DATA::OpenFileForSend( IN LPSTR pszFile)
|
|
/*++
|
|
Open an existing file for transmission using TransmitFile.
|
|
This function converts the given relative path into canonicalized full
|
|
path and opens the file through the cached file handles manager.
|
|
|
|
Arguments:
|
|
pszFile pointer to null-terminated string containing the file name
|
|
|
|
Returns:
|
|
TRUE on success and FALSE if any failure.
|
|
--*/
|
|
{
|
|
APIERR err;
|
|
CHAR szCanonPath[MAX_PATH+1];
|
|
DWORD cbSize = sizeof(szCanonPath);
|
|
CHAR szVirtualPath[MAX_PATH+1];
|
|
DWORD ccbVirtualPath = sizeof(szVirtualPath);
|
|
|
|
DBG_ASSERT( pszFile != NULL );
|
|
|
|
//
|
|
// Close any file we might have open now
|
|
// N.B. There shouldn't be an open file; we're just
|
|
// being careful here.
|
|
//
|
|
|
|
if (m_pOpenFileInfo) {
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"WARNING!! Closing [%08x], before opening %s\n",
|
|
pszFile
|
|
));
|
|
DBG_REQUIRE( CloseFileForSend() );
|
|
}
|
|
|
|
//
|
|
// Open the requested file
|
|
//
|
|
err = VirtualCanonicalize(szCanonPath,
|
|
&cbSize,
|
|
pszFile,
|
|
AccessTypeRead,
|
|
NULL,
|
|
szVirtualPath,
|
|
&ccbVirtualPath);
|
|
|
|
if( err == NO_ERROR ) {
|
|
|
|
DWORD dwCreateFlags = 0;
|
|
|
|
IF_DEBUG( VIRTUAL_IO ) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"Opening File: %s\n", szCanonPath ));
|
|
}
|
|
|
|
// store the virtual path name of file.
|
|
SetVirtualFileName( szVirtualPath );
|
|
|
|
dwCreateFlags = TS_FORBID_SHORT_NAMES | TS_NOT_IMPERSONATED ;
|
|
|
|
|
|
if ( !m_pMetaData || m_pMetaData->QueryDoCache() )
|
|
{
|
|
dwCreateFlags |= TS_CACHING_DESIRED;
|
|
}
|
|
|
|
m_pOpenFileInfo = TsCreateFile( m_pInstance->GetTsvcCache(),
|
|
szCanonPath,
|
|
QueryImpersonationToken(),
|
|
dwCreateFlags
|
|
);
|
|
// caching desired.
|
|
|
|
if( m_pOpenFileInfo == NULL ) {
|
|
|
|
err = GetLastError();
|
|
|
|
} else {
|
|
|
|
DWORD dwAttrib = m_pOpenFileInfo->QueryAttributes();
|
|
|
|
FacIncrement( FacFilesOpened);
|
|
|
|
DBG_ASSERT( dwAttrib != 0xffffffff);
|
|
|
|
if (dwAttrib == 0xFFFFFFFF || // invalid attributes
|
|
dwAttrib & (FILE_ATTRIBUTE_DIRECTORY |
|
|
FILE_ATTRIBUTE_HIDDEN |
|
|
FILE_ATTRIBUTE_SYSTEM)
|
|
) {
|
|
|
|
FacIncrement( FacFilesInvalid);
|
|
|
|
err = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if( err != NO_ERROR ) {
|
|
|
|
IF_DEBUG( VIRTUAL_IO ) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"cannot open %s, error %lu\n",
|
|
pszFile,
|
|
err ));
|
|
}
|
|
}
|
|
|
|
return ( err);
|
|
} // USER_DATA::OpenFileForSend()
|
|
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::CloseFileForSend( IN DWORD dwError, IN BOOL NoLog)
|
|
{
|
|
BOOL fReturn = TRUE;
|
|
TS_OPEN_FILE_INFO * pOpenFileInfo;
|
|
|
|
// make sure it includes the full path
|
|
DBG_ASSERT( m_rgchFile[0] == '/');
|
|
|
|
pOpenFileInfo = (TS_OPEN_FILE_INFO *) InterlockedExchangePointer(
|
|
(PVOID *) &m_pOpenFileInfo,
|
|
NULL
|
|
);
|
|
|
|
if ( pOpenFileInfo != NULL) {
|
|
|
|
//
|
|
// Fabricate an appropriate reply code based on the incoming
|
|
// error code. WriteLogRecord() will pick up this reply code
|
|
// and use it in the activity log.
|
|
//
|
|
|
|
SetLastReplyCode(
|
|
( dwError == NO_ERROR )
|
|
? REPLY_TRANSFER_OK
|
|
: REPLY_TRANSFER_ABORTED
|
|
);
|
|
|
|
FacIncrement( FacFilesClosed);
|
|
TsCloseHandle( m_pInstance->GetTsvcCache(), pOpenFileInfo);
|
|
if ( !NoLog) {
|
|
WriteLogRecord( PSZ_SENT_VERB, m_rgchFile, dwError);
|
|
}
|
|
}
|
|
|
|
return ( fReturn);
|
|
} // USER_DATA::CloseFileForSend()
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::CloseFileForReceive( IN DWORD dwError, IN BOOL NoLog)
|
|
{
|
|
|
|
BOOL fAsyncFileOp;
|
|
fAsyncFileOp = TEST_UF( this, ASYNC_UPLOAD);
|
|
CLEAR_UF( this, ASYNC_UPLOAD );
|
|
|
|
if ( fAsyncFileOp) {
|
|
|
|
m_AioDataFile.StopIo(dwError);
|
|
|
|
//
|
|
// Fabricate an appropriate reply code based on the incoming
|
|
// error code. WriteLogRecord() will pick up this reply code
|
|
// and use it in the activity log.
|
|
//
|
|
|
|
if ( !NoLog) {
|
|
SetLastReplyCode(
|
|
( dwError == NO_ERROR )
|
|
? REPLY_TRANSFER_OK
|
|
: REPLY_TRANSFER_ABORTED
|
|
);
|
|
|
|
WriteLogRecord( m_pszCmdVerb, m_rgchFile, dwError);
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
} // USER_DATA::CloseFileForReceive()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# define MAX_ERROR_MESSAGE_LEN ( 500)
|
|
VOID
|
|
USER_DATA::WriteLogRecord( IN LPCSTR pszVerb,
|
|
IN LPCSTR pszPath,
|
|
IN DWORD dwError)
|
|
/*++
|
|
This function writes the log record for current request made to the
|
|
Ftp server by the client.
|
|
|
|
Arguments:
|
|
pszVerb - pointer to null-terminated string containing the verb
|
|
of operation done
|
|
pszPath - pointer to string containing the path for the verb
|
|
dwError - DWORD containing the error code for operation
|
|
|
|
Returns:
|
|
None.
|
|
--*/
|
|
{
|
|
INETLOG_INFORMATION ilRequest;
|
|
DWORD dwLog;
|
|
CHAR pszClientHostName[50];
|
|
CHAR pszServerIpAddress[50];
|
|
CHAR rgchRequest[MAX_PATH + 20];
|
|
INT cch;
|
|
static CHAR szFTPVersion[]="FTP";
|
|
|
|
BOOL fDontLog = m_pMetaData && m_pMetaData->DontLog();
|
|
|
|
if (!fDontLog)
|
|
{
|
|
|
|
//
|
|
// Fill in the information that needs to be logged.
|
|
//
|
|
|
|
ZeroMemory(&ilRequest, sizeof(ilRequest));
|
|
|
|
P_strncpy( pszClientHostName, (char *)QueryClientHostName(), 50);
|
|
ilRequest.pszClientHostName = pszClientHostName;
|
|
ilRequest.cbClientHostName = strlen(pszClientHostName);
|
|
|
|
ilRequest.pszClientUserName = (char *)QueryUserName();
|
|
P_strncpy( pszServerIpAddress, inet_ntoa( LocalIpAddress ), 50);
|
|
ilRequest.pszServerAddress = pszServerIpAddress;
|
|
|
|
ilRequest.msTimeForProcessing = QueryProcessingTime();
|
|
ilRequest.dwBytesSent = (DWORD)m_licbSent; // BUGBUG: need to log 64 bit
|
|
ilRequest.dwBytesRecvd = (DWORD)m_licbRecvd; // BUGBUG: need to log 64 bit
|
|
ilRequest.dwProtocolStatus = GetLastReplyCode();
|
|
ilRequest.dwWin32Status = dwError;
|
|
ilRequest.dwPort = ntohs ((WORD)LocalIpPort);
|
|
|
|
cch = _snprintf( rgchRequest, sizeof( rgchRequest ), "[%d]%s", QueryId(), pszVerb);
|
|
rgchRequest[ sizeof( rgchRequest) - 1] = '\0';
|
|
DBG_ASSERT( cch > 0 );
|
|
DBG_ASSERT( cch < MAX_PATH + 20);
|
|
|
|
ilRequest.pszOperation = rgchRequest;
|
|
if ( rgchRequest != NULL ) {
|
|
ilRequest.cbOperation = strlen(rgchRequest);
|
|
} else {
|
|
ilRequest.cbOperation = 0;
|
|
}
|
|
|
|
ilRequest.pszTarget = (char *)pszPath;
|
|
if ( pszPath != NULL ) {
|
|
ilRequest.cbTarget = strlen((char *)pszPath);
|
|
} else {
|
|
ilRequest.cbTarget = 0;
|
|
}
|
|
|
|
ilRequest.pszParameters = "";
|
|
ilRequest.pszVersion = szFTPVersion;
|
|
|
|
dwLog = m_pInstance->m_Logging.LogInformation( &ilRequest);
|
|
|
|
if ( dwLog != NO_ERROR) {
|
|
IF_DEBUG( ERROR) {
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
" Unable to log information to logger. Error = %u\n",
|
|
dwLog));
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
" Request From %s, User %s. Request = %s %s\n",
|
|
ilRequest.pszClientHostName,
|
|
ilRequest.pszClientUserName,
|
|
ilRequest.pszOperation,
|
|
ilRequest.pszTarget));
|
|
}
|
|
}
|
|
|
|
//
|
|
// LogInformation() should not fail.
|
|
// If it does fail, the TsvcInfo will gracefully suspend logging
|
|
// for now.
|
|
// We may want to gracefully handle the same.
|
|
//
|
|
}
|
|
|
|
m_licbRecvd = 0; // reset since we wrote the record
|
|
|
|
m_pInstance->QueryStatsObj()->UpdateTotalBytesSent( m_licbSent );
|
|
m_licbSent = 0;
|
|
|
|
return;
|
|
} // USER_DATA::WriteLogRecord()
|
|
|
|
VOID
|
|
USER_DATA::WriteLogRecordForSendError( DWORD dwError )
|
|
{
|
|
//
|
|
// We put this into its own method in this file so it can access
|
|
// the common PSZ_SENT_VERB global.
|
|
//
|
|
|
|
WriteLogRecord(
|
|
PSZ_SENT_VERB,
|
|
m_rgchFile,
|
|
dwError
|
|
);
|
|
|
|
} // USER_DATA::WriteLogRecordForSendError
|
|
|
|
|
|
|
|
//
|
|
// Private functions.
|
|
//
|
|
|
|
VOID
|
|
USER_DATA::CloseSockets(IN BOOL fWarnUser)
|
|
/*++
|
|
Closes sockets (data and control) opened by the user for this session.
|
|
|
|
Arguments:
|
|
fWarnUser - If TRUE, send the user a warning shot before closing
|
|
the sockets.
|
|
--*/
|
|
{
|
|
SOCKET ControlSocket;
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( this ) );
|
|
|
|
//
|
|
// Close any open sockets. It is very important to set
|
|
// PassiveDataListen socket & ControlSocket to INVALID_SOCKET
|
|
// *before* we actually close the sockets.
|
|
// Since this routine is called to
|
|
// disconnect a user, and may be called from the RPC thread,
|
|
// closing one of the sockets may cause the client thread
|
|
// to unblock and try to access the socket. Setting the
|
|
// values in the per-user area to INVALID_SOCKET before
|
|
// closing the sockets keeps this from being a problem.
|
|
//
|
|
// This was a problem created by the Select or WaitForMultipleObjects()
|
|
// Investigate if such race conditions occur with Asynchronous IO?
|
|
// NYI
|
|
//
|
|
|
|
CleanupPassiveSocket( TRUE );
|
|
|
|
//
|
|
// Get rid of the async io connection used for data transfer.
|
|
//
|
|
|
|
m_AioDataConnection.StopIo( NO_ERROR);
|
|
|
|
ControlSocket = QueryControlSocket();
|
|
|
|
if( ControlSocket != INVALID_SOCKET )
|
|
{
|
|
if( fWarnUser )
|
|
{
|
|
//
|
|
// Since this may be called in a context other than
|
|
// the user we're disconnecting, we cannot rely
|
|
// on the USER_DATA fields. So, we cannot call
|
|
// SockReply, so we'll kludge one together with
|
|
// SockPrintf2.
|
|
//
|
|
|
|
SockPrintf2( this,
|
|
ControlSocket,
|
|
"%d Terminating connection.",
|
|
REPLY_SERVICE_NOT_AVAILABLE );
|
|
}
|
|
|
|
StopControlIo(); // to stop the io on control socket.
|
|
}
|
|
|
|
return;
|
|
|
|
} // USER_DATA::CloseSockets()
|
|
|
|
|
|
/*******************************************************************
|
|
|
|
NAME: UserpGetNextId
|
|
|
|
SYNOPSIS: Returns the next available user id.
|
|
|
|
RETURNS: DWORD - The user id.
|
|
|
|
HISTORY:
|
|
KeithMo 23-Mar-1993 Created.
|
|
|
|
********************************************************************/
|
|
DWORD
|
|
UserpGetNextId(
|
|
VOID
|
|
)
|
|
{
|
|
DWORD userId;
|
|
|
|
// Increment the global counter, avoiding it from becoming 0.
|
|
InterlockedIncrement( (LPLONG ) &p_NextUserId);
|
|
|
|
if ((userId = p_NextUserId) == 0) {
|
|
|
|
InterlockedIncrement( (LPLONG ) &p_NextUserId);
|
|
userId = p_NextUserId;
|
|
}
|
|
|
|
DBG_ASSERT( userId != 0);
|
|
|
|
return userId;
|
|
|
|
} // UserpGetNextId
|
|
|
|
|
|
|
|
|
|
|
|
VOID
|
|
USER_DATA::Print( IN LPCSTR pszMsg) const
|
|
/*++
|
|
|
|
Prints the UserData object in debug mode.
|
|
|
|
History:
|
|
MuraliK 28-March-1995 Created.
|
|
--*/
|
|
{
|
|
|
|
# ifdef CHECK_DBG
|
|
CHAR szBuffer[1000];
|
|
|
|
DBG_REQUIRE( _snprintf( szBuffer, sizeof( szBuffer ),
|
|
"[%d] %s: {%u} \"%s\" State=%u. Ref=%u.\n"
|
|
" Ctrl sock=%u; Atq=%x. Data sock=%u; Atq=%x. CtrlRead=%u\n"
|
|
" LastCmd= \"%s\"\n",
|
|
GetCurrentThreadId(), pszMsg,
|
|
QueryId(), QueryUserName(),
|
|
QueryState(), QueryReference(),
|
|
QueryControlSocket(), m_AioControlConnection.QueryAtqContext(),
|
|
QueryDataSocket(), m_AioDataConnection.QueryAtqContext(),
|
|
TEST_UF( this, CONTROL_READ), m_recvBuffer
|
|
) > 0);
|
|
|
|
OutputDebugString( szBuffer);
|
|
|
|
# endif // CHECK_DBG
|
|
|
|
CHKINFO( ( DBG_CONTEXT,
|
|
" Printing USER_DATA( %08x) Signature: %08x\n"
|
|
" RefCount = %08x; UserState = %08x;\n"
|
|
" ControlSocket = %08x; PassiveL = %08x\n"
|
|
" FileInfo@ = %08x; CurDir( %s) Handle = %08x\n"
|
|
" UserName = %s; UserToken = %08x; UserId = %u\n"
|
|
" Behaviour Flags = %08x; XferType = %d; XferMode = %d\n",
|
|
this, Signature, m_References, UserState,
|
|
QueryControlSocket(), m_sPassiveDataListen,
|
|
m_pOpenFileInfo, QueryCurrentDirectory().QueryStr(), CurrentDirHandle,
|
|
QueryUserName(), UserToken, QueryId(),
|
|
Flags, m_xferType, m_xferMode));
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Local IpAddr = %s; HostIpAddr = %s; DataIpAddr = %s;\n"
|
|
" Port = %d; TimeAtConnection = %08x;\n",
|
|
inet_ntoa( LocalIpAddress), inet_ntoa( HostIpAddress),
|
|
inet_ntoa( DataIpAddress),
|
|
DataPort,
|
|
m_TimeAtConnection));
|
|
|
|
DBGPRINTF(( DBG_CONTEXT, " ASYNC_IO_CONN Control=%08x; Data=%08x\n",
|
|
&m_AioControlConnection, m_AioDataConnection));
|
|
|
|
IF_DEBUG( ASYNC_IO) {
|
|
|
|
# if DBG
|
|
m_AioControlConnection.Print();
|
|
m_AioDataConnection.Print();
|
|
# endif // DBG
|
|
}
|
|
|
|
return;
|
|
} // USER_DATA::Print()
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::VirtualPathAccessCheck(IN ACCESS_TYPE _access, IN char * pszPath)
|
|
/*++
|
|
checks to see if the access is allowed for accessing the path
|
|
using pszPath after canonicalizing it.
|
|
|
|
Arguments:
|
|
access the access desired
|
|
pszPath pointer to string containing the path
|
|
|
|
Returns:
|
|
TRUE on success and FALSE if there is any failure.
|
|
|
|
--*/
|
|
{
|
|
DWORD dwError;
|
|
CHAR rgchPath[MAX_PATH+1];
|
|
DWORD dwSize = sizeof(rgchPath);
|
|
|
|
|
|
// this following call converts the symbolic path into absolute
|
|
// and also does path access check.
|
|
dwError = VirtualCanonicalize(rgchPath, &dwSize,
|
|
pszPath, _access);
|
|
|
|
return ( dwError);
|
|
|
|
} // USER_DATA::VirtualPathAccessCheck()
|
|
|
|
|
|
|
|
|
|
|
|
APIERR
|
|
USER_DATA::VirtualCanonicalize(
|
|
OUT CHAR * pszDest,
|
|
IN OUT LPDWORD lpdwSize,
|
|
IN OUT CHAR * pszSearchPath,
|
|
IN ACCESS_TYPE _access,
|
|
OUT LPDWORD pdwAccessMask,
|
|
OUT CHAR * pchVirtualPath, /* OPTIONAL */
|
|
IN OUT LPDWORD lpccbVirtualPath /* OPTIONAL */
|
|
)
|
|
/*++
|
|
This function canonicalizes the path, taking into account the current
|
|
user's current directory value.
|
|
|
|
Arguments:
|
|
pszDest string that will on return contain the complete
|
|
canonicalized path. This buffer will be of size
|
|
specified in *lpdwSize.
|
|
|
|
lpdwSize Contains the size of the buffer pszDest on entry.
|
|
On return contains the number of bytes written
|
|
into the buffer or number of bytes required (incl. \0).
|
|
|
|
pszSearchPath pointer to string containing the path to be converted.
|
|
IF NULL, use the current directory only
|
|
|
|
accesss Access type for this path ( read, write, etc.)
|
|
|
|
pdwAccessMask pointer to DWORD which on succesful deciphering
|
|
will contain the access mask.
|
|
|
|
pchVirtualPath pointer to string which will contain the sanitized
|
|
virtual path on return (on success)
|
|
lpccbVirtualPath pointer to DWORD containing the length of buffer
|
|
(contains the length on return) incl. \0.
|
|
|
|
Returns:
|
|
|
|
Win32 Error Code - NO_ERROR on success
|
|
|
|
MuraliK 24-Apr-1995 Created.
|
|
|
|
--*/
|
|
{
|
|
DWORD dwError = NO_ERROR;
|
|
CHAR rgchVirtual[MAX_PATH+1];
|
|
|
|
DBG_ASSERT( pszDest != NULL);
|
|
DBG_ASSERT( lpdwSize != NULL);
|
|
DBG_ASSERT( pszSearchPath != NULL);
|
|
|
|
IF_DEBUG( VIRTUAL_IO) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"UserData(%08x)::VirtualCanonicalize(%08x, %08x[%u],"
|
|
" %s, %d)\n",
|
|
this, pszDest, lpdwSize, *lpdwSize, pszSearchPath, _access));
|
|
}
|
|
|
|
if ( pdwAccessMask != NULL) {
|
|
|
|
*pdwAccessMask = 0;
|
|
}
|
|
|
|
//
|
|
// Form the virtual path for the given path.
|
|
//
|
|
|
|
if ( !IS_PATH_SEP( *pszSearchPath)) {
|
|
|
|
const CHAR * pszNewDir = QueryCurrentDirectory().QueryStr(); // get virtual dir.
|
|
|
|
//
|
|
// This is a relative path. append it to currrent directory
|
|
//
|
|
|
|
if ( _snprintf( rgchVirtual, sizeof(rgchVirtual) - 1, "%s/%s",
|
|
pszNewDir, pszSearchPath) < 0) {
|
|
|
|
// long path --> is not supported.
|
|
DBGPRINTF((DBG_CONTEXT, "Long Virtual Path %s---%s\n",
|
|
pszNewDir, pszSearchPath));
|
|
|
|
dwError = ERROR_PATH_NOT_FOUND;
|
|
} else {
|
|
|
|
rgchVirtual[ sizeof(rgchVirtual) - 1 ] = '\0';
|
|
pszSearchPath = rgchVirtual;
|
|
}
|
|
} else {
|
|
|
|
// This is an absolute virtual path.
|
|
// need to overwrite this virtual path with absolute
|
|
// path of the root. Do nothing.
|
|
}
|
|
|
|
if ( dwError == NO_ERROR) {
|
|
|
|
DWORD dwAccessMask = 0;
|
|
DBG_ASSERT( IS_PATH_SEP(*pszSearchPath));
|
|
|
|
//
|
|
// Now we have the complete symbolic path to the target file.
|
|
// Translate it into the absolute path
|
|
//
|
|
|
|
VirtualpSanitizePath( pszSearchPath);
|
|
|
|
if ( !LookupVirtualRoot( pszSearchPath,
|
|
pszDest,
|
|
lpdwSize,
|
|
&dwAccessMask ) ) {
|
|
|
|
dwError = GetLastError();
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"LookupVirtualRoot Failed. Error = %d. pszDest = %s. BReq=%d\n",
|
|
dwError, pszDest, *lpdwSize));
|
|
|
|
} else if ( !PathAccessCheck( _access, dwAccessMask,
|
|
TEST_UF( this, READ_ACCESS),
|
|
TEST_UF( this, WRITE_ACCESS))
|
|
) {
|
|
|
|
dwError = GetLastError();
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"PathAccessCheck Failed. Error = %d. pszDest = %s\n",
|
|
dwError, pszDest));
|
|
} else if ( lpccbVirtualPath != NULL) {
|
|
|
|
// successful in getting the path.
|
|
|
|
DWORD cchVPath = strlen( pszSearchPath);
|
|
|
|
*lpccbVirtualPath = cchVPath + 1; // set the length to required size.
|
|
|
|
if ( *lpccbVirtualPath > cchVPath && pchVirtualPath != NULL) {
|
|
|
|
// copy the virtual path, since we have space.
|
|
CopyMemory( pchVirtualPath, pszSearchPath, cchVPath + 1);
|
|
} else {
|
|
dwError = ERROR_INSUFFICIENT_BUFFER;
|
|
}
|
|
|
|
}
|
|
|
|
if ( dwError == NO_ERROR ) {
|
|
// IP check
|
|
|
|
AC_RESULT acIpAccess;
|
|
AC_RESULT acDnsAccess;
|
|
BOOL fNeedDnsCheck;
|
|
|
|
BindPathAccessCheck();
|
|
acIpAccess = QueryAccessCheck()->CheckIpAccess( &fNeedDnsCheck );
|
|
|
|
if ( (acIpAccess == AC_IN_DENY_LIST) ||
|
|
((acIpAccess == AC_NOT_IN_GRANT_LIST) && !fNeedDnsCheck) ) {
|
|
dwError = ERROR_INCORRECT_ADDRESS;
|
|
}
|
|
else if ( fNeedDnsCheck ) {
|
|
if ( !QueryAccessCheck()->IsDnsResolved() ) {
|
|
BOOL fSync;
|
|
LPSTR pDns;
|
|
|
|
if ( !QueryAccessCheck()->QueryDnsName( &fSync,
|
|
(ADDRCHECKFUNCEX)NULL,
|
|
(ADDRCHECKARG)NULL,
|
|
&pDns ) ) {
|
|
dwError = ERROR_INCORRECT_ADDRESS;
|
|
}
|
|
}
|
|
if ( dwError == NO_ERROR ) {
|
|
acDnsAccess = QueryAccessCheck()->CheckDnsAccess();
|
|
|
|
if ( (acDnsAccess == AC_IN_DENY_LIST) ||
|
|
(acDnsAccess == AC_NOT_IN_GRANT_LIST) ||
|
|
((m_acCheck == AC_NOT_IN_GRANT_LIST) &&
|
|
(acDnsAccess != AC_IN_GRANT_LIST) ) ) {
|
|
dwError = ERROR_INCORRECT_ADDRESS;
|
|
}
|
|
}
|
|
}
|
|
UnbindPathAccessCheck();
|
|
}
|
|
|
|
if ( pdwAccessMask != NULL) {
|
|
|
|
*pdwAccessMask = dwAccessMask;
|
|
}
|
|
}
|
|
|
|
|
|
IF_DEBUG( VIRTUAL_IO) {
|
|
|
|
if ( dwError != NO_ERROR) {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
" Cannot Canonicalize %s -- %s, Error = %lu\n",
|
|
QueryCurrentDirectory().QueryStr(),
|
|
pszSearchPath,
|
|
dwError));
|
|
} else {
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"Canonicalized path is: %s\n",
|
|
pszDest));
|
|
}
|
|
}
|
|
|
|
if ( dwError != NO_ERROR ) {
|
|
SetLastError( dwError );
|
|
}
|
|
|
|
return ( dwError);
|
|
} // USER_DATA::VirtualCanonicalize()
|
|
|
|
|
|
|
|
|
|
|
|
/*******************************************************************
|
|
|
|
********************************************************************/
|
|
SOCKERR
|
|
USER_DATA::EstablishDataConnection(
|
|
IN LPCSTR pszReason,
|
|
IN LPCSTR pszSize
|
|
)
|
|
/*++
|
|
|
|
Connects to the client's data socket.
|
|
|
|
Arguments:
|
|
pszReason - The reason for the transfer (file list, get, put, etc).
|
|
pszSize - size of data being transferred.
|
|
|
|
Returns:
|
|
socket error code on any error.
|
|
--*/
|
|
{
|
|
SOCKERR serr = 0;
|
|
SOCKET DataSocket = INVALID_SOCKET;
|
|
BOOL fPassive;
|
|
BOOL fAcceptableSocket = FALSE;
|
|
|
|
//
|
|
// if we're in passive mode and aren't dealing with a fake IO completion [ie reprocessing
|
|
// the command], we just set up the event that will get signalled when the client
|
|
// actually connects.
|
|
//
|
|
|
|
if ( TEST_UF( this, PASSIVE ) &&
|
|
!QueryInFakeIOCompletion() )
|
|
{
|
|
//
|
|
// Ensure we actually created a passive listen data socket.
|
|
// no data transfer socket is in AsyncIo object.
|
|
//
|
|
|
|
DBG_ASSERT( m_sPassiveDataListen != INVALID_SOCKET );
|
|
|
|
//
|
|
// To avoid blocking while waiting for the client to connect, we're going to use
|
|
// WSAEventSelect() to wait for the socket to be accept()'able.
|
|
//
|
|
//
|
|
|
|
if ( ( serr = AddPASVAcceptEvent( &fAcceptableSocket ) ) != 0 )
|
|
{
|
|
ReplyToUser( this,
|
|
REPLY_LOCAL_ERROR,
|
|
PSZ_TOO_MANY_PASV_USERS );
|
|
|
|
return ( serr );
|
|
}
|
|
|
|
//
|
|
// No need to wait around, we can call accept() on the socket right now
|
|
//
|
|
if ( fAcceptableSocket )
|
|
{
|
|
goto continue_label;
|
|
}
|
|
|
|
m_fWaitingForPASVConn = TRUE;
|
|
m_fHavePASVConn = FALSE;
|
|
|
|
return ERROR_IO_PENDING;
|
|
}
|
|
|
|
DBG_ASSERT( !TEST_UF(this, PASSIVE) || QueryInFakeIOCompletion() );
|
|
|
|
|
|
continue_label:
|
|
//
|
|
// Reset any oob flag.
|
|
//
|
|
|
|
CLEAR_UF( this, OOB_DATA );
|
|
|
|
//
|
|
// Capture the user's passive flag, then reset to FALSE.
|
|
//
|
|
|
|
fPassive = TEST_UF( this, PASSIVE );
|
|
CLEAR_UF( this, PASSIVE );
|
|
|
|
//
|
|
// If we're in passive mode, then accept a connection to
|
|
// the data socket.
|
|
//
|
|
// Calling accept() on this socket should -not- block because shouldn't get this
|
|
// far without being sure that calling accept() won't block - that's the point of
|
|
// jumping through the WSAEventSelect() hoops mentioned above
|
|
//
|
|
|
|
if( fPassive )
|
|
{
|
|
|
|
SOCKADDR_IN saddrClient;
|
|
|
|
//
|
|
// Ensure we actually created a passive listen data socket.
|
|
// no data transfer socket is in AsyncIo object.
|
|
//
|
|
|
|
DBG_ASSERT( m_sPassiveDataListen != INVALID_SOCKET );
|
|
|
|
//
|
|
// Wait for a connection.
|
|
//
|
|
|
|
IF_DEBUG( CLIENT )
|
|
{
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"waiting for passive connection on socket %d\n",
|
|
m_sPassiveDataListen ));
|
|
}
|
|
|
|
serr = AcceptSocket( m_sPassiveDataListen,
|
|
&DataSocket,
|
|
&saddrClient,
|
|
TRUE,
|
|
m_pInstance ); // enforce timeouts
|
|
|
|
|
|
//
|
|
// We can kill m_sPassiveDataListen now.
|
|
// We only allow one connection in passive mode.
|
|
//
|
|
|
|
CleanupPassiveSocket( TRUE );
|
|
|
|
// PASV Theft is disabled, so you MUST have the same IP
|
|
// address ad the Control Connection. The flag is renamed to
|
|
// enable_pasv_conn_from_3rdip
|
|
if (!(QueryInstance()->IsEnablePasvConnFrom3rdIP()))
|
|
{
|
|
if (!(HostIpAddress.S_un.S_addr == saddrClient.sin_addr.S_un.S_addr))
|
|
{
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"Unmatching IP - Control: %d.%d.%d.%d Data: %d.%d.%d.%d \n",
|
|
HostIpAddress.S_un.S_un_b.s_b1,
|
|
HostIpAddress.S_un.S_un_b.s_b2,
|
|
HostIpAddress.S_un.S_un_b.s_b3,
|
|
HostIpAddress.S_un.S_un_b.s_b4,
|
|
saddrClient.sin_addr.S_un.S_un_b.s_b1,
|
|
saddrClient.sin_addr.S_un.S_un_b.s_b2,
|
|
saddrClient.sin_addr.S_un.S_un_b.s_b3,
|
|
saddrClient.sin_addr.S_un.S_un_b.s_b4));
|
|
|
|
CloseSocket( DataSocket);
|
|
DataSocket = INVALID_SOCKET;
|
|
serr = WSA_OPERATION_ABORTED;
|
|
};
|
|
};
|
|
|
|
if( serr == 0 )
|
|
{
|
|
|
|
//
|
|
// Got one.
|
|
//
|
|
|
|
DBG_ASSERT( DataSocket != INVALID_SOCKET );
|
|
|
|
m_fHavePASVConn = TRUE;
|
|
m_fWaitingForPASVConn = FALSE;
|
|
|
|
FacIncrement( FacPassiveDataConnections);
|
|
|
|
if ( m_AioDataConnection.SetNewSocket( DataSocket))
|
|
{
|
|
|
|
ReplyToUser(this,
|
|
REPLY_TRANSFER_STARTING,
|
|
PSZ_TRANSFER_STARTING);
|
|
}
|
|
else
|
|
{
|
|
|
|
//
|
|
// We are possibly running low on resources. Send error.
|
|
//
|
|
|
|
ReplyToUser( this,
|
|
REPLY_LOCAL_ERROR,
|
|
PSZ_INSUFFICIENT_RESOURCES);
|
|
|
|
CloseSocket( DataSocket);
|
|
DataSocket = INVALID_SOCKET;
|
|
serr = WSAENOBUFS;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
IF_DEBUG( CLIENT )
|
|
{
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"cannot wait for connection, error %d\n",
|
|
serr ));
|
|
}
|
|
|
|
ReplyToUser(this,
|
|
REPLY_TRANSFER_ABORTED,
|
|
PSZ_TRANSFER_ABORTED);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
|
|
//
|
|
// Announce our intentions of establishing a connection.
|
|
//
|
|
|
|
ReplyToUser(this,
|
|
REPLY_OPENING_CONNECTION,
|
|
PSZ_OPENING_DATA_CONNECTION,
|
|
TransferType(m_xferType ),
|
|
pszReason,
|
|
pszSize);
|
|
|
|
//
|
|
// Open data socket.
|
|
//
|
|
|
|
serr = CreateDataSocket(&DataSocket, // Will receive socket
|
|
LocalIpAddress.s_addr, // Local address
|
|
CONN_PORT_TO_DATA_PORT(LocalIpPort),
|
|
DataIpAddress.s_addr,// RemoteAddr
|
|
DataPort ); // Remote port
|
|
|
|
if ( serr == 0 )
|
|
{
|
|
|
|
DBG_ASSERT( DataSocket != INVALID_SOCKET );
|
|
|
|
FacIncrement( FacActiveDataConnections);
|
|
|
|
if ( !m_AioDataConnection.SetNewSocket( DataSocket))
|
|
{
|
|
|
|
CloseSocket( DataSocket);
|
|
DataSocket = INVALID_SOCKET;
|
|
|
|
serr = WSAENOBUFS;
|
|
}
|
|
}
|
|
|
|
if ( serr != 0)
|
|
{
|
|
|
|
ReplyToUser(this,
|
|
REPLY_CANNOT_OPEN_CONNECTION,
|
|
PSZ_CANNOT_OPEN_DATA_CONNECTION);
|
|
|
|
IF_DEBUG( COMMANDS )
|
|
{
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"could not create data socket, error %d\n",
|
|
serr ));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if( serr == 0 )
|
|
{
|
|
|
|
// set this to indicate a transfer might start
|
|
SET_UF( this, TRANSFER );
|
|
|
|
//
|
|
// Submit a read command on control socket, since we
|
|
// have to await possibility of an abort on OOB_INLINE.
|
|
// Can we ignore possibility of an error on read request?
|
|
//
|
|
|
|
if ( !ReadCommand())
|
|
{
|
|
|
|
DWORD dwError = GetLastError();
|
|
|
|
# ifdef CHECK_DBG
|
|
CHAR szBuffer[100];
|
|
DBG_REQUIRE( _snprintf( szBuffer, sizeof( szBuffer ),
|
|
" Read while DataTfr failed Error = %u. ", dwError) > 0);
|
|
Print( szBuffer);
|
|
# endif // CHECK_DBG
|
|
|
|
IF_DEBUG(CLIENT) {
|
|
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
" %08x::ReadCommand() failed. Error = %u\n",
|
|
this, dwError));
|
|
SetLastError( dwError);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return ( serr);
|
|
|
|
} // USER_DATA::EstablishDataConnection()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
USER_DATA::DestroyDataConnection( IN DWORD dwError)
|
|
/*++
|
|
Tears down the connection to the client's data socket that was created
|
|
using EstablishDataConnection()
|
|
|
|
Arguments:
|
|
dwError = NO_ERROR if data is transferred successfully.
|
|
Win32 error code otherwise
|
|
|
|
--*/
|
|
{
|
|
UINT replyCode;
|
|
LPCSTR pszReply;
|
|
BOOL fTransfer;
|
|
|
|
fTransfer = TEST_UF( this, TRANSFER);
|
|
CLEAR_UF( this, TRANSFER );
|
|
|
|
CleanupPASVFlags();
|
|
|
|
//
|
|
// Close the data socket.
|
|
//
|
|
|
|
DBG_ASSERT( m_sPassiveDataListen == INVALID_SOCKET);
|
|
|
|
|
|
// Stop Io occuring on data connection
|
|
m_AioDataConnection.StopIo(dwError);
|
|
|
|
if ( fTransfer) {
|
|
|
|
//
|
|
// Tell the client we're done with the transfer.
|
|
//
|
|
|
|
if ( dwError == NO_ERROR) {
|
|
|
|
replyCode = REPLY_TRANSFER_OK;
|
|
pszReply = PSZ_TRANSFER_COMPLETE;
|
|
} else {
|
|
|
|
replyCode = REPLY_TRANSFER_ABORTED;
|
|
pszReply = PSZ_TRANSFER_ABORTED;
|
|
}
|
|
|
|
ReplyToUser(this, replyCode, pszReply);
|
|
}
|
|
|
|
return (TRUE);
|
|
} // USER_DATA::DestroyDataConnection()
|
|
|
|
|
|
APIERR
|
|
USER_DATA::GetFileSize()
|
|
{
|
|
LARGE_INTEGER FileSize;
|
|
DWORD dwError = NO_ERROR;
|
|
TS_OPEN_FILE_INFO * pOpenFileInfo;
|
|
CHAR rgchSize[MAX_FILE_SIZE_SPEC];
|
|
|
|
pOpenFileInfo = m_pOpenFileInfo;
|
|
|
|
|
|
if ( pOpenFileInfo == NULL) {
|
|
|
|
return ( ERROR_FILE_NOT_FOUND);
|
|
}
|
|
|
|
if ( !pOpenFileInfo->QuerySize(FileSize)) {
|
|
|
|
dwError = GetLastError();
|
|
|
|
if( dwError != NO_ERROR ) {
|
|
|
|
return ( dwError);
|
|
}
|
|
}
|
|
|
|
IsLargeIntegerToDecimalChar( &FileSize, rgchSize);
|
|
|
|
ReplyToUser( this, REPLY_FILE_STATUS, rgchSize );
|
|
return(dwError);
|
|
}
|
|
|
|
APIERR
|
|
USER_DATA::GetFileModTime(LPSYSTEMTIME lpSystemTime)
|
|
{
|
|
DWORD dwError = NO_ERROR;
|
|
TS_OPEN_FILE_INFO * pOpenFileInfo;
|
|
FILETIME FileTime;
|
|
|
|
pOpenFileInfo = m_pOpenFileInfo;
|
|
|
|
DBG_ASSERT( pOpenFileInfo != NULL );
|
|
|
|
if ( !pOpenFileInfo->QueryLastWriteTime(&FileTime)) {
|
|
|
|
dwError = GetLastError();
|
|
return ( dwError);
|
|
}
|
|
|
|
if (!FileTimeToSystemTime(&FileTime, lpSystemTime)) {
|
|
|
|
return GetLastError();
|
|
}
|
|
|
|
return NO_ERROR;
|
|
}
|
|
|
|
|
|
APIERR
|
|
USER_DATA::SendFileToUser( IN LPSTR pszFileName,
|
|
IN OUT LPBOOL pfErrorSent)
|
|
/*++
|
|
This is a worker function for RETR command of FTP. It will establish
|
|
connection via the ( new ) data socket, then send a file over that
|
|
socket. This uses Async io for transmitting the file.
|
|
|
|
Arguments:
|
|
pszFileName pointer to null-terminated string containing the filename
|
|
pfErrorSent pointer to boolean flag indicating if an error has
|
|
been already sent to client.
|
|
The flag should be used only when return value is error.
|
|
|
|
Returns:
|
|
NO_ERROR on success and Win32 error code if error.
|
|
|
|
History:
|
|
30-April-1995 MuraliK
|
|
--*/
|
|
{
|
|
LARGE_INTEGER FileSize;
|
|
DWORD dwError = NO_ERROR;
|
|
BOOL fTransmit;
|
|
TS_OPEN_FILE_INFO * pOpenFileInfo;
|
|
CHAR rgchSize[MAX_FILE_SIZE_SPEC];
|
|
CHAR rgchBuffer[MAX_FILE_SIZE_SPEC + 10];
|
|
BOOL fDisconnectSocket = TRUE;
|
|
|
|
|
|
DBG_ASSERT( pszFileName != NULL && pfErrorSent != NULL);
|
|
|
|
*pfErrorSent = FALSE;
|
|
|
|
IF_DEBUG( SEND) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" USER_DATA ( %08x)::SendFileToUser( %s,"
|
|
" pfErrorSent = %08x).\n",
|
|
this, pszFileName, pfErrorSent));
|
|
}
|
|
|
|
//
|
|
// Get file size.
|
|
//
|
|
pOpenFileInfo = m_pOpenFileInfo;
|
|
|
|
if ( pOpenFileInfo == NULL) {
|
|
|
|
return ( ERROR_FILE_NOT_FOUND);
|
|
}
|
|
|
|
// Get the file size
|
|
|
|
if ( !pOpenFileInfo->QuerySize(FileSize)) {
|
|
|
|
dwError = GetLastError();
|
|
|
|
if( dwError != NO_ERROR ) {
|
|
|
|
return ( dwError);
|
|
}
|
|
}
|
|
|
|
FileSize.QuadPart -= QueryCurrentOffset();
|
|
|
|
IsLargeIntegerToDecimalChar( &FileSize, rgchSize);
|
|
DBG_REQUIRE( _snprintf( rgchBuffer, sizeof( rgchBuffer ), "(%s bytes)", rgchSize) > 0);
|
|
|
|
m_pInstance->QueryStatsObj()->IncrTotalFilesSent();
|
|
|
|
//
|
|
// Blast the file from a local file to the user.
|
|
//
|
|
|
|
Reference(); // incr ref since async data transfer is started
|
|
SET_UF( this, ASYNC_TRANSFER);
|
|
|
|
if (FileSize.QuadPart > MAX_TRANSMIT_FILE_DATA_CHUNK) {
|
|
//
|
|
// TransmitFile does not handle more than 2GB very well, so we'll break it into
|
|
// 2GB chunks
|
|
//
|
|
|
|
FileSize.QuadPart = MAX_TRANSMIT_FILE_DATA_CHUNK;
|
|
SET_UF( this, ASYNC_DOWNLOAD);
|
|
fDisconnectSocket = FALSE;
|
|
}
|
|
|
|
fTransmit = ( m_AioDataConnection.
|
|
TransmitFileTs( pOpenFileInfo,
|
|
FileSize, // cbToSend ( send entire file)
|
|
QueryCurrentOffset(),
|
|
fDisconnectSocket)
|
|
);
|
|
|
|
if ( !fTransmit) {
|
|
|
|
dwError = GetLastError();
|
|
|
|
IF_DEBUG( SEND) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Unable to transmit file ( %s) (pOpenFile = %p)."
|
|
" Error = %u\n",
|
|
pszFileName,
|
|
pOpenFileInfo,
|
|
dwError));
|
|
}
|
|
|
|
// decr refcount since async tfr failed.
|
|
DBG_REQUIRE( DeReference() > 0);
|
|
}
|
|
|
|
//
|
|
// Disconnect from client.
|
|
// ( will be done at the call back after completion of IO).
|
|
//
|
|
|
|
return ( dwError);
|
|
|
|
} // USER_DATA::SendFileToUser()
|
|
|
|
|
|
|
|
APIERR
|
|
USER_DATA::ReceiveFileFromUser(
|
|
LPSTR pszFileName,
|
|
LPHANDLE phFile
|
|
)
|
|
/*++
|
|
|
|
NAME: ReceiveFileFromUser
|
|
|
|
SYNOPSIS: Worker function for STOR, STOU, and APPE commands.
|
|
Will establish a connection via the (new) data
|
|
socket, then initiate asynchronous receive a file.
|
|
|
|
ENTRY: pszFileName - The name of the file to receive.
|
|
|
|
phFile - An handle to the file being received.
|
|
This handle is closed before this
|
|
routine returns.
|
|
|
|
Returns:
|
|
Win32 Error codes (or socket errors) as DWORD
|
|
|
|
HISTORY:
|
|
KeithMo 16-Mar-1993 Created.
|
|
MuraliK 05-April-1995 Dont free hFile here +
|
|
Alloc IoTransBuffer locally
|
|
RobSol 04-September-2001 convert file upload to be asynchronous
|
|
|
|
--*/
|
|
{
|
|
BOOL fCloseFileOnError = TRUE;
|
|
DWORD dwError = NO_ERROR;
|
|
|
|
DBG_ASSERT( pszFileName != NULL );
|
|
DBG_ASSERT( *phFile != INVALID_HANDLE_VALUE );
|
|
|
|
//
|
|
// Allocate an i/o buffer if not already allocated.
|
|
//
|
|
|
|
if (m_AsyncTransferBuff == NULL) {
|
|
m_AsyncTransferBuff = TCP_ALLOC( g_SocketBufferSize );
|
|
}
|
|
|
|
if( m_AsyncTransferBuff == NULL ) {
|
|
|
|
ReplyToUser(this,
|
|
REPLY_LOCAL_ERROR,
|
|
PSZ_INSUFFICIENT_RESOURCES);
|
|
|
|
dwError = ERROR_NOT_ENOUGH_MEMORY;
|
|
goto ReceiveFileFromUser_exit;
|
|
}
|
|
|
|
if ( !m_AioDataFile.SetNewFile( *phFile)) {
|
|
ReplyToUser(this,
|
|
REPLY_LOCAL_ERROR,
|
|
PSZ_INSUFFICIENT_RESOURCES);
|
|
|
|
dwError = GetLastError();
|
|
goto ReceiveFileFromUser_exit;
|
|
}
|
|
|
|
//
|
|
// after setting the file handle in m_AioDataFile, it will be closed by DestroyDataConnection
|
|
// so we no longer close it.
|
|
//
|
|
fCloseFileOnError = FALSE;
|
|
|
|
Reference(); // incr ref since async data transfer is started
|
|
SET_UF( this, ASYNC_TRANSFER);
|
|
SET_UF( this, ASYNC_UPLOAD);
|
|
|
|
//
|
|
// kick of the data pump by initiating read of the first data block.
|
|
//
|
|
|
|
if ( !m_AioDataConnection.ReadFile(m_AsyncTransferBuff,
|
|
g_SocketBufferSize) ) {
|
|
|
|
dwError = GetLastError();
|
|
|
|
DBG_ASSERT( dwError != NO_ERROR );
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" Unable to receive file ( %s)."
|
|
" Error = %u\n",
|
|
pszFileName,
|
|
dwError));
|
|
|
|
// decr refcount since async recv failed.
|
|
DBG_REQUIRE( DeReference() > 0);
|
|
}
|
|
|
|
ReceiveFileFromUser_exit:
|
|
|
|
if (dwError != NO_ERROR) {
|
|
|
|
//
|
|
// Close file handle before disconnecting from client. This is to serialize
|
|
// requests. If we disconnect first, then an append to this file may follow
|
|
// which may result in a sharing violation of the file (if this write has
|
|
// not been flushed and closed yet).
|
|
//
|
|
|
|
if( *phFile != INVALID_HANDLE_VALUE ) {
|
|
|
|
if ( fCloseFileOnError ) {
|
|
|
|
DBG_REQUIRE( CloseHandle( *phFile ) );
|
|
}
|
|
|
|
*phFile = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
//
|
|
// Disconnect from client.
|
|
//
|
|
|
|
DBG_REQUIRE( DestroyDataConnection( dwError));
|
|
DBG_REQUIRE( CloseFileForReceive( dwError));
|
|
|
|
} else {
|
|
|
|
QueryInstance()->QueryStatsObj()->IncrTotalFilesReceived();
|
|
}
|
|
|
|
return (dwError);
|
|
} // ReceiveFileFromUser()
|
|
|
|
|
|
|
|
|
|
VOID
|
|
USER_DATA::SetPassiveSocket( IN SOCKET sPassive )
|
|
/*++
|
|
|
|
This function frees up an old Passive socket and resets the
|
|
passive socket to the new Passive socket.
|
|
|
|
Arguments:
|
|
sPassive - new passive socket to use
|
|
|
|
--*/
|
|
{
|
|
|
|
SOCKET sPassiveOld;
|
|
|
|
sPassiveOld = (SOCKET) InterlockedExchangePointer ( (PVOID *) &m_sPassiveDataListen,
|
|
(PVOID) sPassive);
|
|
|
|
if ( sPassiveOld != INVALID_SOCKET) {
|
|
|
|
FacDecrement( FacPassiveDataListens);
|
|
DBG_REQUIRE( CloseSocket( sPassiveOld) == 0);
|
|
}
|
|
|
|
if ( sPassive != INVALID_SOCKET) {
|
|
|
|
FacIncrement(FacPassiveDataListens);
|
|
}
|
|
|
|
|
|
return;
|
|
} // USER_DATA::SetPassiveSocket()
|
|
|
|
VOID
|
|
USER_DATA::CleanupPassiveSocket( BOOL fTellWatchThread )
|
|
/*++
|
|
|
|
This function cleans up the resources associated with the current passive socket
|
|
|
|
Arguments:
|
|
fTellWatchThread - flag indicating whether to tell thread waiting for an event on
|
|
the current passive socket to clean up as well
|
|
|
|
Returns:
|
|
Nothing
|
|
--*/
|
|
{
|
|
LockUser();
|
|
|
|
if ( m_sPassiveDataListen == INVALID_SOCKET )
|
|
{
|
|
UnlockUser();
|
|
|
|
return;
|
|
}
|
|
|
|
RemovePASVAcceptEvent( fTellWatchThread );
|
|
|
|
DBG_REQUIRE( CloseSocket( m_sPassiveDataListen ) == 0 );
|
|
|
|
m_sPassiveDataListen = INVALID_SOCKET;
|
|
|
|
UnlockUser();
|
|
}
|
|
|
|
|
|
BOOL
|
|
USER_DATA::SetCommand( IN LPSTR pszCmd )
|
|
/*++
|
|
|
|
Routine Description:
|
|
Used to set pointer to FTP cmd
|
|
|
|
Arguments :
|
|
pszArgs - pointer to command to execute
|
|
|
|
Returns :
|
|
BOOL indicating success/failure to set values
|
|
--*/
|
|
{
|
|
BOOL fReturn = TRUE;
|
|
|
|
if ( !pszCmd )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Free any previous allocations
|
|
//
|
|
if ( m_pszCmd )
|
|
{
|
|
TCP_FREE( m_pszCmd );
|
|
m_pszCmd = NULL;
|
|
}
|
|
|
|
if ( m_pszCmd = ( LPSTR ) TCP_ALLOC( strlen(pszCmd) + 1 ) )
|
|
{
|
|
strcpy( m_pszCmd, pszCmd );
|
|
}
|
|
else
|
|
{
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"Failed to allocate memory for command args !\n"));
|
|
|
|
fReturn = FALSE;
|
|
}
|
|
|
|
return fReturn;
|
|
}
|
|
|
|
/************************************************************
|
|
* Auxiliary Functions
|
|
************************************************************/
|
|
|
|
|
|
VOID
|
|
ProcessUserAsyncIoCompletion(IN LPVOID pContext,
|
|
IN DWORD cbIo,
|
|
IN DWORD dwError,
|
|
IN LPASYNC_IO_CONNECTION pAioConn,
|
|
IN BOOL fTimedOut
|
|
)
|
|
/*++
|
|
This function processes the Async Io completion ( invoked as
|
|
a callback from the ASYNC_IO_CONNECTION object).
|
|
|
|
Arguments:
|
|
pContext pointer to the context information ( UserData object).
|
|
cbIo count of bytes transferred in Io
|
|
dwError DWORD containing the error code resulting from last tfr.
|
|
pAioConn pointer to AsyncIo connection object.
|
|
|
|
Returns:
|
|
None
|
|
--*/
|
|
{
|
|
|
|
LPUSER_DATA pUserData = (LPUSER_DATA ) pContext;
|
|
|
|
DBG_ASSERT( pUserData != NULL);
|
|
DBG_ASSERT( pAioConn != NULL);
|
|
|
|
IF_SPECIAL_DEBUG( CRITICAL_PATH) {
|
|
|
|
CHAR rgchBuffer[100];
|
|
|
|
DBG_REQUIRE( _snprintf( rgchBuffer, sizeof( rgchBuffer ),
|
|
" ProcessAio( cb=%u, err=%u, Aio=%p). ",
|
|
cbIo, dwError, pAioConn) > 0);
|
|
|
|
pUserData->Print( rgchBuffer);
|
|
}
|
|
|
|
DBG_REQUIRE( pUserData->Reference() > 0);
|
|
|
|
# if DBG
|
|
|
|
if ( !IS_VALID_USER_DATA( pUserData)) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
"Encountering an invalid user data ( %08x)\n",
|
|
pUserData));
|
|
pUserData->Print();
|
|
}
|
|
# endif // DBG
|
|
|
|
DBG_ASSERT( IS_VALID_USER_DATA( pUserData ) );
|
|
|
|
pUserData->ProcessAsyncIoCompletion( cbIo, dwError, pAioConn, fTimedOut);
|
|
|
|
DereferenceUserDataAndKill(pUserData);
|
|
|
|
return;
|
|
|
|
} // ProcessUserAsyncIoCompletion()
|
|
|
|
|
|
VOID
|
|
USER_DATA::RemovePASVAcceptEvent( BOOL fTellWatchThread )
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Routine that cleans up the state associated with a PASV accept event
|
|
|
|
Arguments:
|
|
|
|
fTellWatchThread - BOOL indicating whether or not to inform the thread waiting on
|
|
the event to stop waiting on it
|
|
|
|
Returns:
|
|
|
|
Nothing
|
|
--*/
|
|
{
|
|
DBG_ASSERT( m_sPassiveDataListen != INVALID_SOCKET );
|
|
|
|
if ( m_hPASVAcceptEvent == NULL )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Remove all network notifications for the PASV socket
|
|
//
|
|
if ( WSAEventSelect( m_sPassiveDataListen,
|
|
m_hPASVAcceptEvent,
|
|
0 ) )
|
|
{
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"WSAEventSelect on socket %d failed : 0x%x\n",
|
|
m_sPassiveDataListen, WSAGetLastError()));
|
|
}
|
|
|
|
//
|
|
// Stop watching for the event
|
|
//
|
|
if ( fTellWatchThread )
|
|
{
|
|
RemoveAcceptEvent( m_hPASVAcceptEvent,
|
|
this );
|
|
}
|
|
|
|
WSACloseEvent( m_hPASVAcceptEvent );
|
|
|
|
m_hPASVAcceptEvent = NULL;
|
|
}
|
|
|
|
SOCKERR
|
|
USER_DATA::AddPASVAcceptEvent( BOOL *pfAcceptableSocket )
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Routine that sets up the event to signal that the PASV socket is an accept()'able
|
|
state
|
|
|
|
Arguments:
|
|
|
|
pfAcceptableSocket - BOOL set to TRUE if socket can be accept()'ed at once, FALSE if
|
|
NOT
|
|
|
|
Returns:
|
|
|
|
Error code indicating success/failure
|
|
|
|
--*/
|
|
{
|
|
DWORD dwRet = 0;
|
|
SOCKERR serr = 0;
|
|
BOOL fRegistered = FALSE;
|
|
|
|
*pfAcceptableSocket = FALSE;
|
|
|
|
if ( ( m_hPASVAcceptEvent = WSACreateEvent() ) == WSA_INVALID_EVENT )
|
|
{
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"Failed to create event to wait for accept() : 0x%x\n",
|
|
WSAGetLastError()));
|
|
|
|
return WSAGetLastError();
|
|
}
|
|
|
|
//
|
|
// specify that we want to be alerted when the socket is accept()'able =)
|
|
//
|
|
if ( WSAEventSelect( m_sPassiveDataListen,
|
|
m_hPASVAcceptEvent,
|
|
FD_ACCEPT ) )
|
|
{
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"WSAEventSelect failed : 0x%x\n",
|
|
WSAGetLastError()));
|
|
|
|
serr = WSAGetLastError();
|
|
|
|
goto exit;
|
|
}
|
|
else
|
|
{
|
|
fRegistered = TRUE;
|
|
}
|
|
|
|
//
|
|
// In order to deal as quickly as possible with legitimate clients and avoid rejecting them
|
|
// because the queue is full, we'll wait for 0.1 sec to see whether the socket becomes
|
|
// accept()'able before queueing it
|
|
//
|
|
dwRet = WSAWaitForMultipleEvents( 1,
|
|
&m_hPASVAcceptEvent,
|
|
FALSE,
|
|
100,
|
|
FALSE );
|
|
|
|
switch ( dwRet )
|
|
{
|
|
case WSA_WAIT_EVENT_0:
|
|
//
|
|
// we can call accept() at once on the socket, no need to muck around with waiting
|
|
// for it
|
|
//
|
|
|
|
*pfAcceptableSocket = TRUE;
|
|
|
|
break;
|
|
|
|
case WSA_WAIT_TIMEOUT:
|
|
//
|
|
// Need to queue the socket
|
|
//
|
|
serr = AddAcceptEvent( m_hPASVAcceptEvent,
|
|
this );
|
|
|
|
break;
|
|
|
|
default:
|
|
serr = WSAGetLastError();
|
|
|
|
break;
|
|
}
|
|
|
|
exit:
|
|
|
|
//
|
|
// clean up if something failed or the socket is acceptible
|
|
//
|
|
if ( (serr != 0) || *pfAcceptableSocket )
|
|
{
|
|
if ( m_hPASVAcceptEvent )
|
|
{
|
|
if ( fRegistered )
|
|
{
|
|
WSAEventSelect( m_sPassiveDataListen,
|
|
m_hPASVAcceptEvent,
|
|
0 );
|
|
}
|
|
|
|
WSACloseEvent( m_hPASVAcceptEvent );
|
|
|
|
m_hPASVAcceptEvent = NULL;
|
|
}
|
|
}
|
|
|
|
return serr;
|
|
}
|
|
|
|
|
|
|
|
|
|
VOID
|
|
DereferenceUserDataAndKill(IN OUT LPUSER_DATA pUserData)
|
|
/*++
|
|
This function dereferences User data and kills the UserData object if the
|
|
reference count hits 0. Before killing the user data, it also removes
|
|
the connection from the list of active connections.
|
|
|
|
--*/
|
|
{
|
|
|
|
FTP_SERVER_INSTANCE * pinstance;
|
|
|
|
IF_SPECIAL_DEBUG( CRITICAL_PATH) {
|
|
|
|
pUserData->Print( " Deref ");
|
|
}
|
|
|
|
//
|
|
// We must capture the instance pointer from the user data, as
|
|
// USER_DATA::RemoveConnection() will set the pointer to NULL.
|
|
// We must also reference the instance before locking it, as
|
|
// removing the last user from the instance will cause the instance
|
|
// to be destroyed. We'll defer this destruction until we're done
|
|
// with the instance.
|
|
//
|
|
|
|
pinstance = pUserData->QueryInstance();
|
|
|
|
pinstance->Reference();
|
|
pinstance->LockConnectionsList();
|
|
|
|
if ( !pUserData->DeReference()) {
|
|
|
|
//
|
|
// Deletion of the object USER_DATA is required.
|
|
//
|
|
|
|
IF_DEBUG( USER_DATABASE) {
|
|
|
|
DBGPRINTF( ( DBG_CONTEXT,
|
|
" UserData( %08x) is being deleted.\n",
|
|
pUserData));
|
|
}
|
|
|
|
pinstance->UnlockConnectionsList();
|
|
|
|
pUserData->Cleanup();
|
|
|
|
DBG_ASSERT( pUserData->QueryControlSocket() == INVALID_SOCKET );
|
|
DBG_ASSERT( pUserData->QueryDataSocket() == INVALID_SOCKET );
|
|
|
|
pinstance->RemoveConnection( pUserData);
|
|
}
|
|
else {
|
|
|
|
pinstance->UnlockConnectionsList();
|
|
}
|
|
|
|
pinstance->Dereference();
|
|
|
|
} // DereferenceUserDataAndKill()
|
|
|
|
|
|
|
|
|
|
BOOL
|
|
PathAccessCheck(IN ACCESS_TYPE _access,
|
|
IN DWORD dwVrootAccessMask,
|
|
IN BOOL fUserRead,
|
|
IN BOOL fUserWrite
|
|
)
|
|
/*++
|
|
This function determines if the required privilege to access the specified
|
|
virtual root with a given access mask exists.
|
|
|
|
Arguments:
|
|
|
|
access - specifies type of acces desired.
|
|
dwVrootAccessMask - DWORD containing the access mask for the virtual root.
|
|
fUserRead - user's permission to read (general)
|
|
fUserWrite - user's permission to write (general)
|
|
|
|
Returns:
|
|
BOOL - TRUE if access is to be granted, else FALSE.
|
|
|
|
History:
|
|
MuraliK 20-Sept-1995
|
|
|
|
--*/
|
|
{
|
|
BOOL fAccessGranted = FALSE;
|
|
|
|
DBG_ASSERT( IS_VALID_ACCESS_TYPE( _access ) );
|
|
|
|
//
|
|
// Perform the actual access check.
|
|
//
|
|
|
|
switch( _access ) {
|
|
|
|
case AccessTypeRead :
|
|
|
|
fAccessGranted = (fUserRead &&
|
|
((dwVrootAccessMask & VROOT_MASK_READ)
|
|
== VROOT_MASK_READ)
|
|
);
|
|
break;
|
|
|
|
case AccessTypeWrite :
|
|
case AccessTypeCreate :
|
|
case AccessTypeDelete :
|
|
|
|
fAccessGranted = (fUserWrite &&
|
|
((dwVrootAccessMask & VROOT_MASK_WRITE)
|
|
== VROOT_MASK_WRITE)
|
|
);
|
|
break;
|
|
|
|
default :
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"PathAccessCheck - invalid access type %d\n",
|
|
_access ));
|
|
DBG_ASSERT( FALSE );
|
|
break;
|
|
}
|
|
|
|
if (!fAccessGranted) {
|
|
|
|
SetLastError( ERROR_ACCESS_DENIED);
|
|
}
|
|
|
|
return ( fAccessGranted);
|
|
} // PathAccessCheck()
|
|
|
|
|
|
VOID SignalAcceptableSocket( LPUSER_DATA pUserData )
|
|
/*++
|
|
|
|
Function that restarts processing the original command when a PASV data socket becomes
|
|
accept()'able [ie the client has made the connection]
|
|
|
|
Arguments:
|
|
pUserData - USER_DATA context attached to socket
|
|
|
|
Returns:
|
|
Nothing
|
|
--*/
|
|
{
|
|
PATQ_CONTEXT pAtqContext = pUserData->QueryControlAio()->QueryAtqContext();
|
|
|
|
//
|
|
// Stop waiting for events on this socket
|
|
//
|
|
pUserData->RemovePASVAcceptEvent( FALSE );
|
|
|
|
pUserData->SetInFakeIOCompletion( TRUE );
|
|
|
|
//
|
|
// do a scary thing - fake an IO completion, to trigger re-processing of the FTP command
|
|
//
|
|
if ( !AtqPostCompletionStatus( pAtqContext,
|
|
strlen( pUserData->QueryCmdString() ) + 1 ) )
|
|
{
|
|
DBGPRINTF((DBG_CONTEXT,
|
|
"Failed to post fake completion status to deal with PASV event : 0x%x\n",
|
|
GetLastError()));
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
VOID CleanupTimedOutSocketContext( LPUSER_DATA pUserData )
|
|
/*++
|
|
|
|
Function used to do cleanup when timeout for waiting for a PASV connection expires
|
|
|
|
Arguments:
|
|
pUserData - context pointer
|
|
|
|
Returns:
|
|
Nothing
|
|
--*/
|
|
{
|
|
DBG_ASSERT( pUserData );
|
|
|
|
pUserData->LockUser();
|
|
|
|
pUserData->CleanupPassiveSocket( FALSE );
|
|
|
|
CLEAR_UF( pUserData, PASSIVE );
|
|
|
|
pUserData->SetHavePASVConn( FALSE );
|
|
|
|
pUserData->SetWaitingForPASVConn( FALSE );
|
|
|
|
ReplyToUser( pUserData,
|
|
REPLY_CANNOT_OPEN_CONNECTION,
|
|
PSZ_CANNOT_OPEN_DATA_CONNECTION );
|
|
//
|
|
// Remove our reference to this USER_DATA object
|
|
//
|
|
pUserData->DeReference();
|
|
|
|
pUserData->UnlockUser();
|
|
}
|
|
|
|
/*******************************************************************
|
|
|
|
NAME: FtpMetaDataFree
|
|
|
|
SYNOPSIS: Frees a formatted meta data object when it's not in use.
|
|
|
|
ENTRY: pObject - Pointer to the meta data object.
|
|
|
|
RETURNS:
|
|
|
|
|
|
NOTES:
|
|
|
|
|
|
********************************************************************/
|
|
|
|
VOID
|
|
FtpMetaDataFree(
|
|
PVOID pObject
|
|
)
|
|
{
|
|
PFTP_METADATA pMD;
|
|
|
|
pMD = (PFTP_METADATA)pObject;
|
|
|
|
delete pMD;
|
|
}
|
|
|
|
|
|
BOOL
|
|
FTP_METADATA::HandlePrivateProperty(
|
|
LPSTR pszURL,
|
|
PIIS_SERVER_INSTANCE pInstance,
|
|
METADATA_GETALL_INTERNAL_RECORD *pMDRecord,
|
|
LPVOID pDataPointer,
|
|
BUFFER *pBuffer,
|
|
DWORD *pdwBytesUsed,
|
|
PMETADATA_ERROR_INFO pMDErrorInfo
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Handle metabase properties private to FTP service
|
|
|
|
Arguments:
|
|
|
|
pszURL - URL of the requested object
|
|
pInstance - FTP server instance
|
|
pMDRecord - metadata record
|
|
pDataPointer - pointer to metabase data
|
|
pBuffer - Buffer available for storage space
|
|
pdwBytesUsed - Pointer to bytes used in *pBuffer
|
|
|
|
Returns:
|
|
|
|
BOOL - TRUE success ( or not handled ), otherwise FALSE.
|
|
|
|
--*/
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL
|
|
FTP_METADATA::FinishPrivateProperties(
|
|
BUFFER *pBuffer,
|
|
DWORD dwBytesUsed,
|
|
BOOL bSucceeded
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Handles completion of reading metabase properties private to FTP.
|
|
|
|
Arguments:
|
|
|
|
pBuffer - Buffer previously used for storage space
|
|
dwBytesUsed - bytes used in *pBuffer
|
|
|
|
Returns:
|
|
|
|
BOOL - TRUE success ( or not handled ), otherwise FALSE.
|
|
|
|
--*/
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOL
|
|
USER_DATA::LookupVirtualRoot(
|
|
IN const CHAR * pszURL,
|
|
OUT CHAR * pszPath,
|
|
OUT DWORD * pccbDirRoot,
|
|
OUT DWORD * pdwAccessMask
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Looks up the virtual root to find the physical drive mapping. If an
|
|
Accept-Language header was sent by the client, we look for a virtual
|
|
root prefixed by the language tag
|
|
|
|
Arguments:
|
|
|
|
pstrPath - Receives physical drive path
|
|
pszURL - URL to look for
|
|
pccbDirRoot - On entry, size of pstrPath buffer in bytes. On return,
|
|
Number of characters in the found physical path (incl. \0)
|
|
pdwMask - Access mask for the specified URL
|
|
|
|
Returns:
|
|
BOOL - TRUE if success, otherwise FALSE.
|
|
|
|
--*/
|
|
{
|
|
PFTP_METADATA pMD;
|
|
DWORD dwDataSetNumber;
|
|
PVOID pCacheInfo;
|
|
MB mb( (IMDCOM*) g_pInetSvc->QueryMDObject() );
|
|
STACK_STR( strFullPath, MAX_PATH );
|
|
METADATA_ERROR_INFO MDErrorInfo;
|
|
BOOL fOk;
|
|
DWORD dwError = NO_ERROR;
|
|
|
|
|
|
|
|
if ( m_pMetaData != NULL )
|
|
{
|
|
TsFreeMetaData( m_pMetaData->QueryCacheInfo() );
|
|
m_pMetaData = NULL;
|
|
}
|
|
|
|
// First read the data set number, and see if we already have it
|
|
// cached. We don't do a full open in this case.
|
|
|
|
if ( !strFullPath.Copy( m_pInstance->QueryMDVRPath() ) ||
|
|
!strFullPath.Append( ( *pszURL == '/' ) ? pszURL + 1 : pszURL ) )
|
|
{
|
|
goto LookupVirtualRoot_Error;
|
|
}
|
|
|
|
if (!mb.GetDataSetNumber( strFullPath.QueryStr(),
|
|
&dwDataSetNumber ))
|
|
{
|
|
goto LookupVirtualRoot_Error;
|
|
}
|
|
|
|
// See if we can find a matching data set already formatted.
|
|
pMD = (PFTP_METADATA)TsFindMetaData(dwDataSetNumber, METACACHE_FTP_SERVER_ID);
|
|
|
|
if (pMD == NULL)
|
|
{
|
|
pMD = new FTP_METADATA;
|
|
|
|
if (pMD == NULL)
|
|
{
|
|
goto LookupVirtualRoot_Error;
|
|
}
|
|
|
|
if ( !pMD->ReadMetaData( m_pInstance,
|
|
&mb,
|
|
(LPSTR)pszURL,
|
|
&MDErrorInfo) )
|
|
{
|
|
delete pMD;
|
|
goto LookupVirtualRoot_Error;
|
|
}
|
|
|
|
// We were succesfull, so try and add this metadata. There is a race
|
|
// condition where someone else could have added it while we were
|
|
// formatting. This is OK - we'll have two cached, but they should be
|
|
// consistent, and one of them will eventually time out. We could have
|
|
// AddMetaData check for this, and free the new one while returning a
|
|
// pointer to the old one if it finds one, but that isn't worthwhile
|
|
// now.
|
|
|
|
pCacheInfo = TsAddMetaData(pMD, FtpMetaDataFree,
|
|
dwDataSetNumber, METACACHE_FTP_SERVER_ID);
|
|
|
|
}
|
|
|
|
m_pMetaData = pMD;
|
|
|
|
if ( m_pMetaData->QueryVrError() )
|
|
{
|
|
dwError = m_pMetaData->QueryVrError();
|
|
goto LookupVirtualRoot_Error;
|
|
}
|
|
|
|
//
|
|
// Build physical path from VR_PATH & portion of URI not used to define VR_PATH
|
|
//
|
|
|
|
//
|
|
// for URLs containing a Virtual Root, as well as MD_USER_ISOLATION_NONE mode, and StandAlone
|
|
// mode, concatenate the VR physical path and the URL to form a full physical path. In
|
|
// StandAlone where the path does not contain a VR alias, contatenate the physical root path,
|
|
// the user home directory, and the URL. In MD_USER_ISOLATION_AD mode (with no VR prefix),
|
|
// concatenate the user physical home directory (in the USER_DATA objcet) and the relative
|
|
// path.
|
|
//
|
|
|
|
if (( m_pInstance->QueryIsolationMode() == MD_USER_ISOLATION_AD ) &&
|
|
( m_pMetaData->QueryVrLevel() == 0 )) {
|
|
//
|
|
// we get here in AD isolation mode, where the VR has no path mapping
|
|
//
|
|
|
|
if ( !strFullPath.Copy( QueryRootDirectory().QueryStr(),
|
|
QueryRootDirectory().QueryCCH() ) ||
|
|
!strFullPath.Append( pszURL ) )
|
|
{
|
|
goto LookupVirtualRoot_Error;
|
|
}
|
|
|
|
FlipSlashes( strFullPath.QueryStr(), FLIP_SLASHES_TO_DOS );
|
|
|
|
fOk = TRUE;
|
|
} else {
|
|
//
|
|
// for stand-alone UIM (no VROOT), we use the homeDirectory as a relative path prefix.
|
|
// no-isolation and all VROOTs use the VR lookup path.
|
|
//
|
|
|
|
PCSTR pszHomeDir = NULL;
|
|
|
|
if ((m_pMetaData->QueryVrLevel() == 0) &&
|
|
(m_pInstance->QueryIsolationMode() == MD_USER_ISOLATION_BASIC) ) {
|
|
pszHomeDir = m_strRootDir.QueryStr();
|
|
}
|
|
|
|
fOk = pMD->BuildPhysicalPathWithAltRoot( (LPSTR)pszURL, &strFullPath, pszHomeDir );
|
|
}
|
|
|
|
if ( fOk ) {
|
|
if ( *pccbDirRoot > strFullPath.QueryCCH() )
|
|
{
|
|
memcpy( pszPath, strFullPath.QueryStr(), strFullPath.QueryCCH()+1 );
|
|
*pccbDirRoot = strFullPath.QueryCCH()+1;
|
|
if ( pdwAccessMask )
|
|
{
|
|
*pdwAccessMask = m_pMetaData->QueryAccessPerms();
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
LookupVirtualRoot_Error:
|
|
|
|
if (dwError == NO_ERROR) {
|
|
//
|
|
// best error message to send to client
|
|
//
|
|
dwError = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
SetLastError( dwError );
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
BOOL
|
|
USER_DATA::BindInstanceAccessCheck(
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Bind IP/DNS access check for this request to instance data
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Returns:
|
|
|
|
BOOL - TRUE if success, otherwise FALSE.
|
|
|
|
--*/
|
|
{
|
|
if ( m_rfAccessCheck.CopyFrom( m_pInstance->QueryMetaDataRefHandler() ) )
|
|
{
|
|
m_acAccessCheck.BindCheckList( (LPBYTE)m_rfAccessCheck.GetPtr(), m_rfAccessCheck.GetSize() );
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
VOID
|
|
USER_DATA::UnbindInstanceAccessCheck()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Unbind IP/DNS access check for this request to instance data
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Returns:
|
|
|
|
Nothing
|
|
|
|
--*/
|
|
{
|
|
m_acAccessCheck.UnbindCheckList();
|
|
m_rfAccessCheck.Reset( (IMDCOM*) g_pInetSvc->QueryMDObject() );
|
|
}
|
|
|
|
|
|
BOOL
|
|
USER_DATA::IsFileNameShort( IN LPSTR pszFile)
|
|
/*++
|
|
|
|
Check file name beeing short or not.
|
|
|
|
Arguments:
|
|
pszFile pointer to null-terminated string containing the file name
|
|
|
|
Returns:
|
|
TRUE if filename is short.
|
|
--*/
|
|
{
|
|
APIERR err;
|
|
CHAR szCanonPath[MAX_PATH+1];
|
|
DWORD cbSize = sizeof(szCanonPath);
|
|
BOOL fShort;
|
|
BOOL fRet = FALSE;
|
|
|
|
DBG_ASSERT( pszFile != NULL );
|
|
|
|
//
|
|
// Close any file we might have open now
|
|
// N.B. There shouldn't be an open file; we're just
|
|
// being careful here.
|
|
//
|
|
|
|
if (m_pOpenFileInfo) {
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"WARNING!! Closing [%08x], before opening %s\n",
|
|
pszFile
|
|
));
|
|
DBG_REQUIRE( CloseFileForSend() );
|
|
}
|
|
|
|
//
|
|
// Open the requested file
|
|
//
|
|
err = VirtualCanonicalize(szCanonPath,
|
|
&cbSize,
|
|
pszFile,
|
|
AccessTypeRead);
|
|
|
|
if( err == NO_ERROR )
|
|
{
|
|
|
|
if ( strchr( szCanonPath, '~' ))
|
|
{
|
|
|
|
err = CheckIfShortFileName( (UCHAR *) szCanonPath, TsTokenToImpHandle( QueryUserToken()), &fShort );
|
|
|
|
if ( !err && fShort)
|
|
{
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"Short filename being rejected \"%s\"\n",
|
|
szCanonPath ));
|
|
fRet = TRUE;
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return fRet;
|
|
} // USER_DATA::IsFileNameShort()
|
|
|
|
|
|
|
|
DWORD
|
|
USER_DATA::CheckIfShortFileName(
|
|
IN CONST UCHAR * pszPath,
|
|
IN HANDLE hImpersonation,
|
|
OUT BOOL * pfShort
|
|
)
|
|
/*++
|
|
Description:
|
|
|
|
This function takes a suspected NT/Win95 short filename and checks if there's
|
|
an equivalent long filename. For example, c:\foobar\ABCDEF~1.ABC is the same
|
|
as c:\foobar\abcdefghijklmnop.abc.
|
|
|
|
NOTE: This function should be called unimpersonated - the FindFirstFile() must
|
|
be called in the system context since most systems have traverse checking turned
|
|
off - except for the UNC case where we must be impersonated to get network access.
|
|
|
|
Arguments:
|
|
|
|
pszPath - Path to check
|
|
hImpersonation - Impersonation handle if this is a UNC path - can be NULL if not UNC
|
|
pfShort - Set to TRUE if an equivalent long filename is found
|
|
|
|
Returns:
|
|
|
|
Win32 error on failure
|
|
--*/
|
|
{
|
|
DWORD err = NO_ERROR;
|
|
WIN32_FIND_DATA FindData;
|
|
UCHAR * psz;
|
|
BOOL fUNC;
|
|
|
|
psz = _mbschr( (UCHAR *) pszPath, '~' );
|
|
*pfShort = FALSE;
|
|
fUNC = (*pszPath == '\\');
|
|
|
|
//
|
|
// Loop for multiple tildas - watch for a # after the tilda
|
|
//
|
|
|
|
while ( psz++ )
|
|
{
|
|
if ( *psz >= '0' && *psz <= '9' )
|
|
{
|
|
UCHAR achTmp[MAX_PATH];
|
|
UCHAR * pchEndSeg;
|
|
UCHAR * pchBeginSeg;
|
|
HANDLE hFind;
|
|
|
|
//
|
|
// Isolate the path up to the segment with the
|
|
// '~' and do the FindFirst with that path
|
|
//
|
|
|
|
pchEndSeg = _mbschr( psz, '\\' );
|
|
|
|
if ( !pchEndSeg )
|
|
{
|
|
pchEndSeg = psz + _mbslen( psz );
|
|
}
|
|
|
|
//
|
|
// If the string is beyond MAX_PATH then we allow it through
|
|
//
|
|
|
|
if ( ((INT) (pchEndSeg - pszPath)) >= sizeof( achTmp ))
|
|
{
|
|
return NO_ERROR;
|
|
}
|
|
|
|
memcpy( achTmp, pszPath, (INT) (pchEndSeg - pszPath) );
|
|
achTmp[pchEndSeg - pszPath] = '\0';
|
|
|
|
if ( fUNC && hImpersonation )
|
|
{
|
|
if ( !ImpersonateLoggedOnUser( hImpersonation ))
|
|
{
|
|
return GetLastError();
|
|
}
|
|
}
|
|
|
|
hFind = FindFirstFile( (CHAR *) achTmp, &FindData );
|
|
|
|
if ( fUNC && hImpersonation )
|
|
{
|
|
RevertToSelf();
|
|
}
|
|
|
|
if ( hFind == INVALID_HANDLE_VALUE )
|
|
{
|
|
err = GetLastError();
|
|
|
|
DBGPRINTF(( DBG_CONTEXT,
|
|
"FindFirst failed!! - \"%s\", error %d\n",
|
|
achTmp,
|
|
GetLastError() ));
|
|
|
|
//
|
|
// If the FindFirstFile() fails to find the file then return
|
|
// success - the path doesn't appear to be a valid path which
|
|
// is ok.
|
|
//
|
|
|
|
if ( err == ERROR_FILE_NOT_FOUND ||
|
|
err == ERROR_PATH_NOT_FOUND )
|
|
{
|
|
return NO_ERROR;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
DBG_REQUIRE( FindClose( hFind ));
|
|
|
|
//
|
|
// Isolate the last segment of the string which should be
|
|
// the potential short name equivalency
|
|
//
|
|
|
|
pchBeginSeg = _mbsrchr( achTmp, '\\' );
|
|
DBG_ASSERT( pchBeginSeg );
|
|
pchBeginSeg++;
|
|
|
|
//
|
|
// If the last segment doesn't match the long name then this is
|
|
// the short name version of the path
|
|
//
|
|
|
|
if ( _mbsicmp( (UCHAR *) FindData.cFileName, pchBeginSeg ))
|
|
{
|
|
*pfShort = TRUE;
|
|
return NO_ERROR;
|
|
}
|
|
}
|
|
|
|
psz = _mbschr( psz, '~' );
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/******************************* End Of File *************************/
|
|
|