/**********************************************************************/ /** Microsoft Windows NT **/ /** Copyright(c) Microsoft Corp., 1994 **/ /**********************************************************************/ /* doput.cxx This module contains the code for the PUT verb FILE HISTORY: Henrysa 08-May-1996 Created from doget.cxx */ #include "w3p.hxx" #include // // Private constants. // // // Private globals. // CHAR cPutDeleteBody[] = "

%s was %s successfully.

"; DWORD dwPutNumCPU; DWORD dwPutBlockedCount; // // Private prototypes. // BOOL W95MoveFileEx( IN LPCSTR lpExistingFile, IN LPCSTR lpNewFile ); class FILENAME_LOCK; typedef class FILENAME_LOCK *PFILENAME_LOCK; typedef struct _FN_LOCK_TABLE_ENTRY { LIST_ENTRY ListHead; CRITICAL_SECTION csCritSec; } FN_LOCK_TABLE_ENTRY, *PFN_LOCK_TABLE_ENTRY; class FILENAME_LOCK { public: FILENAME_LOCK(STR *pstrFileName, PFN_LOCK_TABLE_ENTRY pTableEntry) : hEvent(NULL), dwWaitCount(0) { if (!strFileName.Copy(*pstrFileName)) { return; } hEvent = IIS_CREATE_EVENT( "FILENAME_LOCK::hEvent", this, FALSE, FALSE ); if (hEvent == NULL) { return; } // It's important that we not insert this into the table until // we know if the event was created OK, because the validity // check only checks for the event handle, and this object // will be freed without being pulled from the table if // the validity check fails. pLockTableEntry = pTableEntry; InsertHeadList(&pTableEntry->ListHead, &ListEntry); } ~FILENAME_LOCK(VOID) { DBG_ASSERT(dwWaitCount == 0); if (hEvent != NULL) { CloseHandle(hEvent); } } BOOL IsValid(VOID) { return hEvent != NULL; } DWORD Acquire(VOID); VOID Release(VOID); PLIST_ENTRY Next(VOID) { return ListEntry.Flink; } BOOL IsEqual(STR *Name) { if (Name->QueryCB() == strFileName.QueryCB() && _strnicmp(Name->QueryStr(), strFileName.QueryStr(), Name->QueryCB() ) == 0) { return TRUE; } return FALSE; } LIST_ENTRY ListEntry; private: HANDLE hEvent; STR strFileName; PFN_LOCK_TABLE_ENTRY pLockTableEntry; DWORD dwWaitCount; }; #define MAX_FN_LOCK_BUCKETS 67 FN_LOCK_TABLE_ENTRY FNLockTable[MAX_FN_LOCK_BUCKETS]; // // Public functions. // BOOL HandledByApp( CHAR *pszURL, PW3_METADATA pMD, enum HTTP_VERB Verb, CHAR *pszVerb ) /*++ Routine Description: Determine if a particular URL/Verb combo might be handled by a serer side app, such as an ISAPI or CGI app. Arguments: pszURL - URL to be checked. pMD - Metadata for the URL Verb - Verb to be checked. pszVerb - String form of Verb. Returns: TRUE if we think it might be handled by an app, FALSE otherwise. --*/ { CHAR *pchExt; STR strDummy; GATEWAY_TYPE Type; DWORD cchExt; BOOL fImageInURL; BOOL fVerbExcluded; DWORD dwScriptMapFlags; PVOID pExtMapInfo; pchExt = pszURL; while (*pchExt) { pchExt = strchr( pchExt + 1, '.' ); pExtMapInfo = NULL; if ( !pMD->LookupExtMap( pchExt, FALSE, &strDummy, &Type, &cchExt, &fImageInURL, &fVerbExcluded, &dwScriptMapFlags, pszVerb, Verb, &pExtMapInfo )) { return FALSE; } if ( pchExt == NULL || (Type != GATEWAY_UNKNOWN && !fVerbExcluded)) { break; } } return !(Type == GATEWAY_UNKNOWN || Type == GATEWAY_NONE); } DWORD HTTP_REQUEST::BuildAllowHeader( CHAR *pszURL, CHAR *pszTail ) /*++ Routine Description: Build an Allow: header appropriate to the URL. Arguments: pszURL - URL to be checked. pszTail - Place to build the allow header. Returns: Number of bytes appended to incoming pszTail. --*/ { CHAR *pszOld = pszTail; BOOL bExecAllowed; if (_pMetaData == NULL) { return 0; } APPEND_STRING(pszTail, "Allow: OPTIONS, TRACE"); bExecAllowed = (IS_ACCESS_ALLOWED(EXECUTE) || _fAnyParams || _pMetaData->QueryAnyExtAllowedOnReadDir() ); if ( (bExecAllowed && HandledByApp(_strURL.QueryStr(), _pMetaData, HTV_GET, "GET") ) || IS_ACCESS_ALLOWED(READ) ) { APPEND_STRING(pszTail, ", GET"); } if ( (bExecAllowed && HandledByApp(_strURL.QueryStr(), _pMetaData, HTV_HEAD, "HEAD") ) || IS_ACCESS_ALLOWED(READ) ) { APPEND_STRING(pszTail, ", HEAD"); } if ( (bExecAllowed && HandledByApp(_strURL.QueryStr(), _pMetaData, HTV_PUT, "PUT") ) || IS_ACCESS_ALLOWED(WRITE) ) { APPEND_STRING(pszTail, ", PUT"); } if ( (bExecAllowed && HandledByApp(_strURL.QueryStr(), _pMetaData, HTV_DELETE, "DELETE") ) || IS_ACCESS_ALLOWED(WRITE) ) { APPEND_STRING(pszTail, ", DELETE"); } if ( bExecAllowed && HandledByApp(_strURL.QueryStr(), _pMetaData, HTV_POST, "POST") ) { APPEND_STRING(pszTail, ", POST"); } APPEND_STRING(pszTail, "\r\n"); return DIFF(pszTail - pszOld); } VOID FILENAME_LOCK::Release( VOID ) /*++ Routine Description: Release a filename lock. If there's noone waiting on the lock, then we're done with it, and we can go ahead and delete it. Otherwise signal the waiter so he can continue. Arguments: None Return Value: None --*/ { EnterCriticalSection(&pLockTableEntry->csCritSec); if (dwWaitCount == 0) { // Nobody's waiting. Pull this guy from the list and delete him. RemoveEntryList(&ListEntry); LeaveCriticalSection(&pLockTableEntry->csCritSec); delete this; } else { DWORD dwRet; // Someone's waiting, so set the event to let then go. LeaveCriticalSection(&pLockTableEntry->csCritSec); dwRet = SetEvent(hEvent); DBG_ASSERT(dwRet != 0); } } DWORD FILENAME_LOCK::Acquire( VOID ) /*++ Routine Description: Acquire a filename lock. This routine is called if a filename lock is already held by another thread. Increment the wait count, and then block on an event. The holder of the lock will signal the event when they're done. This routine is called with the lock table bucket critical section held, and will free it before returning. Arguments: None Return Value: None --*/ { DWORD dwRet; DWORD dwMaxThreads; DWORD dwAlreadyBlocked; // First, make sure we can block. We can block if no more than // 3/4ths of the total number of available threads are already // blocked in us. Note that we don't do this at all under Win95. if (g_fIsWindows95) { LeaveCriticalSection(&pLockTableEntry->csCritSec); return WAIT_TIMEOUT; } dwMaxThreads = (DWORD)AtqGetInfo(AtqMaxPoolThreads) * dwPutNumCPU; dwAlreadyBlocked = InterlockedIncrement((LPLONG)&dwPutBlockedCount) - 1; if (dwAlreadyBlocked < ((dwMaxThreads * 3) / 4)) { // It's OK to block this guy. // Increment the wait count so the holder knows someone is // waiting. dwWaitCount++; LeaveCriticalSection(&pLockTableEntry->csCritSec); // Block on the event until we're signalled or time out. dwRet = WaitForSingleObject(hEvent, g_dwPutEventTimeout * 1000); // We're done waiting. Enter the critical section and decrement // the wait count. We can't do this with interlocked instructions // because because other parts of this package use the critical // section to serialize with the wait count and other actions // atomically. EnterCriticalSection(&pLockTableEntry->csCritSec); dwWaitCount--; } else { dwRet = WAIT_TIMEOUT; } InterlockedDecrement((LPLONG)&dwPutBlockedCount); LeaveCriticalSection(&pLockTableEntry->csCritSec); return dwRet; } // // Private functions. // PFILENAME_LOCK AcquireFileNameLock( STR *pstrFileName ) /*++ Routine Description: Acquire a file name lock after looking it up in the table. We hash the file name, search the appropriate bucket for it, and acquire it. If we don't find one we create a new one. Arguments: strFileName - Name of the file name we're "locking". Return Value: Pointer to the file name lock we return. --*/ { DWORD dwHash; CHAR *pchTemp; PLIST_ENTRY pCurrentEntry; PFILENAME_LOCK pFNLock; // First, hash the file name. dwHash = 0; pchTemp = pstrFileName->QueryStr(); while (*pchTemp != '\0') { dwHash += (DWORD) *pchTemp; dwHash = _rotr(dwHash, 1); pchTemp++; } dwHash = ( (dwHash & 0xff) + ((dwHash >> 8) & 0xff) + ((dwHash >> 16) & 0xff) + ((dwHash >> 24) & 0xff) ) % MAX_FN_LOCK_BUCKETS; // We've hashed it, now take the critical section for this bucket. EnterCriticalSection(&FNLockTable[dwHash].csCritSec); // Walk down the bucket, looking for an existing lock structure. pCurrentEntry = FNLockTable[dwHash].ListHead.Flink; while (pCurrentEntry != &FNLockTable[dwHash].ListHead) { pFNLock = CONTAINING_RECORD(pCurrentEntry, FILENAME_LOCK, ListEntry); // See if this one matches. if (pFNLock->IsEqual(pstrFileName)) { DWORD dwRet; // It is, so try to acquire this one. The acquire routine will free // the critical section for us. dwRet = pFNLock->Acquire(); // Here we're done waiting. If we succeeded, return a pointer to // the lock, otherwise return NULL. if (dwRet == WAIT_OBJECT_0) { return pFNLock; } else { SetLastError(ERROR_INVALID_PARAMETER); return NULL; } } // Wasn't this one, try the next one. pCurrentEntry = pFNLock->Next(); } // If we get here there is no filename lock currently existing, so create // a new one. pFNLock = new FILENAME_LOCK(pstrFileName, &FNLockTable[dwHash]); // Don't need the crit sec anymore, so let it go. LeaveCriticalSection(&FNLockTable[dwHash].csCritSec); if (pFNLock == NULL) { // Couldn't get it. return NULL; } // Make sure the initialization worked. if (!pFNLock->IsValid()) { // It didn't. delete pFNLock; return NULL; } // Otherwise, we're cool, and we own the newly created lock, so we're done. return pFNLock; } BOOL HTTP_REQUEST::BuildPutDeleteHeader( enum WRITE_TYPE Action ) /*++ Routine Description: This is a utility routine to build a response header and body appropriate for a put or delete response. Arguments: Action - The action we just took. Return Value: None --*/ { BOOL fFinished; CHAR *pszTail; CHAR *pszResp; DWORD cbRespBytes; DWORD cbRespBytesNeeded; STACK_STR(strEscapedURL, 256); strEscapedURL.Copy(_strURL); strEscapedURL.Escape(); if (Action == WTYPE_CREATE) { CHAR *pszHostName; CHAR *pszProtocol; DWORD cbProtocolLength; DWORD cbHostNameLength; BOOL bNeedPort; if (!BuildHttpHeader(&fFinished, "201 Created", NULL)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); cbRespBytes = strlen(pszResp); pszHostName = QueryHostAddr(); cbHostNameLength = strlen(pszHostName); // Now we need to add the Location header, which means fully // qualifying the URL. First figure out how many bytes we might // need, then build the Location header. if (IsSecurePort()) { pszProtocol = "https://"; cbProtocolLength = sizeof("https://") - 1; bNeedPort = (INT) QueryClientConn()->QueryPort() != HTTP_SSL_PORT; } else { pszProtocol = "http://"; cbProtocolLength = sizeof("http://") - 1; bNeedPort = (INT) QueryClientConn()->QueryPort() != 80; } cbRespBytesNeeded = cbRespBytes + (sizeof("Location: ")) - 1 + cbProtocolLength + cbHostNameLength + (bNeedPort ? 6 : 0) + // For :port, if needed strEscapedURL.QueryCB() + sizeof("\r\n") - 1; if (!QueryRespBuf()->Resize(cbRespBytesNeeded)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); pszTail = pszResp + cbRespBytes; APPEND_STRING(pszTail, "Location: "); memcpy(pszTail, pszProtocol, cbProtocolLength); pszTail += cbProtocolLength; memcpy(pszTail, pszHostName, cbHostNameLength); pszTail += cbHostNameLength; // Append :port if we need to. if (bNeedPort) { APPEND_STRING(pszTail, ":"); pszTail += sprintf(pszTail, "%u", (UINT)(QueryClientConn()->QueryPort())); } memcpy(pszTail, strEscapedURL.QueryStr(), strEscapedURL.QueryCB()); pszTail += strEscapedURL.QueryCB(); APPEND_STRING(pszTail, "\r\n"); cbRespBytes = cbRespBytesNeeded; } else { if (!BuildBaseResponseHeader(QueryRespBuf(), &fFinished, NULL, 0)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); cbRespBytes = strlen(pszResp); } cbRespBytesNeeded = cbRespBytes + sizeof("Content-Type: text/html\r\n") - 1 + sizeof("Content-Length: ") - 1 + 5 + // For content length digits MAX_ALLOW_SIZE + strEscapedURL.QueryCB() + sizeof("\r\n\r\n") - 1 + 7 + // "written"/"deleted"/"created" sizeof(cPutDeleteBody); if (!QueryRespBuf()->Resize(cbRespBytesNeeded)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); pszTail = pszResp + cbRespBytes; APPEND_STRING(pszTail, "Content-Type: text/html\r\n"); APPEND_STRING(pszTail, "Content-Length: "); pszTail += sprintf(pszTail, "%u\r\n", sizeof(cPutDeleteBody) - 1 + strEscapedURL.QueryCB() + sizeof("deleted") - 1 - (sizeof("%s") - 1) - (sizeof("%s") - 1)); if (Action != WTYPE_DELETE) { pszTail += BuildAllowHeader(_strURL.QueryStr(), pszTail); } APPEND_STRING(pszTail, "\r\n"); sprintf(pszTail, cPutDeleteBody, strEscapedURL.QueryStr(), (Action == WTYPE_CREATE ? "created" : (Action == WTYPE_WRITE ? "written" : "deleted"))); return TRUE; } VOID HTTP_REQUEST::CleanupTempFile( VOID ) /*++ Routine Description: Utility routine to close the temp file handle and delete the file. Arguments: None Return Value: None --*/ { // Impersonate the user who opened the file. If the temp file handle // is still valid, close it and delete the file. if ( ImpersonateUser( ) ) { if ( _hTempFileHandle != INVALID_HANDLE_VALUE ) { ::CloseHandle( _hTempFileHandle ); ::DeleteFile( _strTempFileName.QueryStr( ) ); _hTempFileHandle = INVALID_HANDLE_VALUE; } RevertUser( ); } } VOID HTTP_REQUEST::VerifyMimeType( VOID ) /*++ Routine Description: Make sure that the mime type specified for the current file matches that specified by the client. If it doesn't, create a custom mime mapping for this URL. Arguments: None. Return Value: --*/ { STACK_STR(strMimeType, 80); CHAR *pszClientMimeType; BOOL bHaveMapping; // See if the client specified a mime type. If he didn't, // we're done. // pszClientMimeType = (CHAR * ) _HeaderList.FastMapQueryValue(HM_CTY); if (pszClientMimeType == NULL) { return; // No mime type, system default is OK. } // // Client specified a mime type. See if it matches what we already // have. // bHaveMapping = SelectMimeMapping( &strMimeType, _strPhysicalPath.QueryStr(), _pMetaData); if (!bHaveMapping || _stricmp(strMimeType.QueryStr(), pszClientMimeType)) { // // Either the mime type lookup failed, or what we have doesn't match what // the client specified. In either case create a custom mime map entry // for the specified URL. The custom mime map we create is a default // one, since it's on a specific file. // MB mb( (IMDCOM*) g_pInetSvc->QueryMDObject() ); STACK_STR(strFullMDPath, 128); STACK_STR(strCustomMime, 80); if (!strCustomMime.Copy("*") || !strCustomMime.Append(",", sizeof(",") - 1) || !strCustomMime.Append(pszClientMimeType) ) { return; } // Append the extra NULL. if (!strCustomMime.Resize(strCustomMime.QueryCCH() + 2)) { return; } if (!strCustomMime.SetLen(strCustomMime.QueryCB() + 1)) { return; } // // Construct the full path to the URL, and try and open it. // if (!strFullMDPath.Copy(QueryW3Instance()->QueryMDVRPath(), QueryW3Instance()->QueryMDVRPathLen() - 1) || !strFullMDPath.Append(_strURL)) { // // Couldn't construct the path. // return; } // Now try and open it. if ( !mb.Open( strFullMDPath.QueryStr(), METADATA_PERMISSION_READ | METADATA_PERMISSION_WRITE ) ) { // See what the error was. if (GetLastError() != ERROR_PATH_NOT_FOUND) { // Some error other than not exist, so quit. return; } // The path doesn't exist, so add it. if (!mb.Open(QueryW3Instance()->QueryMDVRPath(), METADATA_PERMISSION_READ | METADATA_PERMISSION_WRITE) ) { return; } if (!mb.AddObject(_strURL.QueryStr()) && GetLastError() != ERROR_ALREADY_EXISTS ) { return; } // We added it. Close the handle so the next guy can get in. DBG_REQUIRE(mb.Close()); // // Now try again to open the full path. // if ( !mb.Open( strFullMDPath.QueryStr(), METADATA_PERMISSION_READ | METADATA_PERMISSION_WRITE ) ) { return; } } mb.SetMultiSZ( "", MD_MIME_MAP, IIS_MD_UT_FILE, strCustomMime.QueryStr()); } } BOOL HTTP_REQUEST::DoPut( BOOL * pfFinished ) /*++ Routine Description: Handle a PUT request ( cf HTTP 1.1 spec ) Check the URL being put, read the entity body and write it to disk, etc. Arguments: pfFinished - Set to TRUE if no further processings is needed for this request Return Value: TRUE if success, else FALSE --*/ { HANDLE hFile; SECURITY_ATTRIBUTES sa; TCHAR szTempFileName[MAX_PATH/sizeof(TCHAR)]; DWORD err; BOOL fDone = FALSE; BOOL fFileSuccess; DWORD cbFileBytesWritten; DWORD dwFileCreateError; BOOL fReturn; BOOL bPrecondWorked; CHAR *pszContentType; switch (_putstate) { case PSTATE_START: // // Make sure the virtual root allows write access // if ( !IS_ACCESS_ALLOWED(WRITE) ) { SetState( HTR_DONE, HT_FORBIDDEN, ERROR_ACCESS_DENIED ); Disconnect( HT_FORBIDDEN, IDS_WRITE_ACCESS_DENIED, FALSE, pfFinished ); return TRUE; } // // Check what we're trying to PUT, make sure we have permission. // if ( !VrootAccessCheck( _pMetaData, FILE_GENERIC_WRITE ) ) { SetDeniedFlags( SF_DENIED_RESOURCE ); return FALSE; } // We don't support Range requests on PUTs. if ( QueryHeaderList()->FindValueInChunks("Content-Range:", sizeof("Content-Range:") - 1) != NULL ) { SetState( HTR_DONE, HT_NOT_SUPPORTED, ERROR_NOT_SUPPORTED ); Disconnect( HT_NOT_SUPPORTED, IDS_PUT_RANGE_UNSUPPORTED, FALSE, pfFinished ); return TRUE; } // Also don't support PUT to the '*' URL. if (*_strURL.QueryStr() == '*') { // Don't allow GETs on the server URL. SetState( HTR_DONE, HT_BAD_REQUEST, ERROR_INVALID_PARAMETER ); Disconnect( HT_BAD_REQUEST, NULL, FALSE, pfFinished ); return TRUE; } // // Make sure we support this content type. We don't understand how to // handle composite MIME types, so fail if it's a message/ content type. // pszContentType = (CHAR * ) _HeaderList.FastMapQueryValue(HM_CTY); if (pszContentType != NULL) { if (!_strnicmp(pszContentType, "message/", sizeof("message/") - 1) ) { SetState( HTR_DONE, HT_NOT_SUPPORTED, ERROR_NOT_SUPPORTED ); Disconnect( HT_NOT_SUPPORTED, IDS_UNSUPPORTED_CONTENT_TYPE, FALSE, pfFinished ); return TRUE; } } // If we don't have a content length or chunked transfer encoding, // fail. if (!_fHaveContentLength && !IsChunked()) { SetState( HTR_DONE, HT_LENGTH_REQUIRED, ERROR_NOT_SUPPORTED ); Disconnect( HT_LENGTH_REQUIRED, IDS_CANNOT_DETERMINE_LENGTH, FALSE, pfFinished ); return TRUE; } // Now get the filename lock. _pFileNameLock = AcquireFileNameLock(&_strPhysicalPath); if (_pFileNameLock == NULL) { // Couldn't get it, return 'resource busy'. //make sure we log the event SetState ( HTR_DONE, HT_SVC_UNAVAILABLE, ERROR_SHARING_VIOLATION ); Disconnect( HT_SVC_UNAVAILABLE, IDS_PUT_CONTENTION, FALSE, pfFinished ); return TRUE; } if ( !ImpersonateUser( ) ) { _pFileNameLock->Release(); _pFileNameLock = NULL; return (FALSE); } sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = FALSE; hFile = ::CreateFile( _strPhysicalPath.QueryStr(), GENERIC_READ | GENERIC_WRITE, g_fIsWindows95 ? (FILE_SHARE_READ | FILE_SHARE_WRITE) : (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), &sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if ( ( hFile != INVALID_HANDLE_VALUE ) && ( GetFileType( hFile ) != FILE_TYPE_DISK ) ) { DBG_REQUIRE( ::CloseHandle( hFile ) ); hFile = INVALID_HANDLE_VALUE; SetLastError( ERROR_ACCESS_DENIED ); } dwFileCreateError = ::GetLastError(); if ( hFile == INVALID_HANDLE_VALUE) { // Some sort of error opening the file. RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; if ( dwFileCreateError == ERROR_FILE_NOT_FOUND || dwFileCreateError == ERROR_PATH_NOT_FOUND) { // The path or filename itself is bad, fail the request. SetState( HTR_DONE, HT_NOT_FOUND, GetLastError() ); Disconnect( HT_NOT_FOUND, NO_ERROR, FALSE, pfFinished ); return TRUE; } if ( dwFileCreateError == ERROR_INVALID_NAME ) { // An invalid name. SetState( HTR_DONE, HT_BAD_REQUEST, GetLastError() ); Disconnect( HT_BAD_REQUEST, NO_ERROR, FALSE, pfFinished ); return TRUE; } else { // A 'bad' error has occured, so return FALSE. if ( dwFileCreateError == ERROR_ACCESS_DENIED ) { SetDeniedFlags( SF_DENIED_RESOURCE ); } } return FALSE; } else { // The create worked. If this isn't a directory and it's not // hidden or readonly we're OK. If we just created it now, delete // it so we don't have to remember to if the put fails later. BY_HANDLE_FILE_INFORMATION FileInfo; // Get file information and check for a directory or hidden file. fReturn = GetFileInformationByHandle( hFile, &FileInfo ); // See if this file is accessible. If not, fail. if ( fReturn) { // We got the information. if ( FileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { DBG_REQUIRE( ::CloseHandle( hFile )); if ( dwFileCreateError != ERROR_ALREADY_EXISTS) { ::DeleteFile( _strPhysicalPath.QueryStr() ); } // Don't allow puts to directory. RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; SetState( HTR_DONE, HT_BAD_REQUEST, GetLastError() ); Disconnect( HT_BAD_REQUEST, NO_ERROR, FALSE, pfFinished ); return TRUE; } if ( FileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) { // Pretend we can't see hidden files. DBG_REQUIRE( ::CloseHandle( hFile )); if ( dwFileCreateError != ERROR_ALREADY_EXISTS) { ::DeleteFile( _strPhysicalPath.QueryStr() ); } RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; SetState( HTR_DONE, HT_NOT_FOUND, GetLastError() ); Disconnect( HT_NOT_FOUND, NO_ERROR, FALSE, pfFinished ); return TRUE; } } else { // Some sort of fatal error. DBG_REQUIRE( ::CloseHandle( hFile )); if ( dwFileCreateError != ERROR_ALREADY_EXISTS) { ::DeleteFile( _strPhysicalPath.QueryStr() ); } RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } } // Check the preconditions on this file, if there are any. It's // possible that it would be better to do this at the very end, // right before we rename the file, but for not we do it here. bPrecondWorked = FALSE; _bFileExisted = (dwFileCreateError == ERROR_ALREADY_EXISTS); if (_fIfModifier) { // Have a modifier. See if it succeeds. If it does, CheckPreconditions // will do a synchronous send of the appropriate response header. bPrecondWorked = CheckPreconditions(hFile, _bFileExisted, pfFinished, &fReturn ); } // Close the handle now, since we know longer need it. DBG_REQUIRE( ::CloseHandle( hFile )); if (!_bFileExisted) { // File didn't originally exist, so delete the one we just created. ::DeleteFile( _strPhysicalPath.QueryStr() ); } if (bPrecondWorked) { _pFileNameLock->Release(); _pFileNameLock = NULL; if (!fReturn) { return fReturn; } // We had a precondition work. If we're read the entire entity // body, we're done. Otherwise we need to keep reading and // discarding the data. CheckPreconditions has already done a synchronous // send of the appropriate headers. if (QueryClientContentLength() <= QueryTotalEntityBodyCB()) { // All done. The call to CheckPreconditions() above should // have set our state to done already. DBG_ASSERT(QueryState() == HTR_DONE); *pfFinished = TRUE; return TRUE; } // Otherwise we need to start reading and discarding. We start the // first read here. If that pends, we just return, leaving our put // state set to discard-read so that we'll do the appropriate // things when the read completes. If the read succeeds now, // we set our put state to discard-chunk and call ourselves // recursively to let the discard-chunk substate handler do the // right things. DBG_ASSERT(!*pfFinished); SetState( HTR_DOVERB, _dwLogHttpResponse, NO_ERROR ); _putstate = PSTATE_DISCARD_READ; if ( !ReadEntityBody( &fDone, TRUE, QueryMetaData()->QueryPutReadSize() )) { return FALSE; } if (!fDone) { return TRUE; } _putstate = PSTATE_DISCARD_CHUNK; return DoPut(pfFinished); } // At this point we've verified the request. We need to generate a // temporary file name and if this is a 1.1 or greater client send // back a 100 Continue response. err = ::GetTempFileName( g_pszW3TempDirName, W3_TEMP_PREFIX, 0, szTempFileName ); if ( err == 0 || !_strTempFileName.Copy( szTempFileName ) ) { // // Couldn't create or copy a temporary file name. // RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } // Now open the temp file. We specify OPEN_EXISTING because we // want to fail if someone's deleted the temp file name before we // opened it. If that's happened we have no real guarantee that the // file is still unique. We don't specify any sharing as we don't // want anyone else to write into while we are. _hTempFileHandle = ::CreateFile( szTempFileName, GENERIC_READ | GENERIC_WRITE, g_fIsWindows95 ? 0 : FILE_SHARE_DELETE, &sa, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); RevertUser(); if (_hTempFileHandle == INVALID_HANDLE_VALUE) { // Uh-oh. Couldn't open the temp file we just created. This is bad, // return FALSE to fail the request rudely. DBGPRINTF((DBG_CONTEXT,"CreateFile[%s] failed with %d\n", szTempFileName, GetLastError())); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } // Now everything's good. If this is a 1.1 client or greater send // back the 100 Continue response, otherwise skip that. if ( IsAtLeastOneOne() ) { if ( !SendHeader( "100 Continue", "\r\n", IO_FLAG_SYNC, pfFinished, HTTPH_NO_CONNECTION) ) { // An error on the header send. Abort this request. CleanupTempFile( ); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } if ( *pfFinished ) { CleanupTempFile(); } } // Set out put state to reading, and begin reading the entity body. _putstate = PSTATE_READING; if ( !ReadEntityBody( &fDone, TRUE, QueryMetaData()->QueryPutReadSize() )) { // Had some sort of problem reading the entity body. This is // fatal. CleanupTempFile( ); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } if ( !fDone ) { return TRUE; } DBG_ASSERT((QueryEntityBodyCB() >= QueryMetaData()->QueryPutReadSize()) || (QueryEntityBodyCB() == QueryClientContentLength())); // Fall through to the PSTATE_READING handler. case PSTATE_READING: // When we get here, we've completed a read. If this isn't the // fall through case call ReadEntityBody again to find out if // we're done with the current chunk. if ( !fDone ) { if ( !ReadEntityBody( &fDone, FALSE, QueryMetaData()->QueryPutReadSize() )) { // Had some sort of problem reading the entity body. This is // fatal. CleanupTempFile( ); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } if ( !fDone ) { return TRUE; } } // Right here we know we've read the current chunk, so we want // to set our state to writing and write this to the temp file. do { DWORD dwAmountToWrite; // Since there could be extra slop in the buffer, we need to // figure out how much to actually write. TotalEntityBodyCB // is the total number of bytes we've read into the buffer // for this request, and EntityBodyCB is the number of bytes // in the buffer for this chunk. Therefore TEBCB - EBCB is the // amount we've already read and written to the file. We want to // write the minimum of the ContentLength - this amount and // EBCB. dwAmountToWrite = min ( QueryClientContentLength() - (QueryTotalEntityBodyCB() - QueryEntityBodyCB()), QueryEntityBodyCB() ); // // Check to see if the amount we're to write is 0, and skip if it // is. This can happen in the chunked case where all we got on // the last read is the 0 terminator. // if (dwAmountToWrite != 0) { fFileSuccess = ::WriteFile( _hTempFileHandle, QueryEntityBody(), dwAmountToWrite, &cbFileBytesWritten, NULL ); if ( !fFileSuccess || cbFileBytesWritten != dwAmountToWrite ) { // Some sort of problem with the write. Abort the request. CleanupTempFile( ); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } } _cbEntityBody = 0; _cbBytesWritten = 0; // Now, if we haven't read all of the entity body try to read some more. if ( QueryClientContentLength() > QueryTotalEntityBodyCB() ) { if ( !ReadEntityBody( &fDone, TRUE, QueryMetaData()->QueryPutReadSize() )) { // Had some sort of problem reading the entity body. This is // fatal. CleanupTempFile( ); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } } else { // We've read and written all of the entity body. Rename the // temp file now, close the temp file handle (causing a delete), // flush the atq cache and we're done. if ( !ImpersonateUser( ) ) { CleanupTempFile( ); _pFileNameLock->Release(); _pFileNameLock = NULL; return (FALSE); } // // If the file we're PUTing already exists, rename it to // another name before we move the other one over. if ( _bFileExisted ) { err = ::GetTempFileName( g_pszW3TempDirName, W3_TEMP_PREFIX, 0, szTempFileName ); if ( err == 0 ) { // // Couldn't create a temporary file name. // RevertUser(); CleanupTempFile(); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } // Now do the actual rename. GetTempFileName() will // have created the file. if ( g_fIsWindows95 ) { fFileSuccess = ::W95MoveFileEx( _strPhysicalPath.QueryStr(), szTempFileName ); } else { fFileSuccess = ::MoveFileEx(_strPhysicalPath.QueryStr(), szTempFileName, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED ); } if ( !fFileSuccess ) { // Couldn't move the file! RevertUser(); CleanupTempFile(); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } } ::CloseHandle( _hTempFileHandle ); _hTempFileHandle = INVALID_HANDLE_VALUE; // Now rename the temp file to the file we're PUTing. The file // shouldn't already exist (since we'd have renamed it if it // did), so this should fail if it does. if ( g_fIsWindows95 ) { fFileSuccess = ::W95MoveFileEx(_strTempFileName.QueryStr(), _strPhysicalPath.QueryStr() ); } else { fFileSuccess = ::MoveFileEx(_strTempFileName.QueryStr(), _strPhysicalPath.QueryStr(), MOVEFILE_COPY_ALLOWED ); } _pFileNameLock->Release(); _pFileNameLock = NULL; if ( fFileSuccess ) { BOOL fHandled; DWORD cbDummy; // The rename worked. Cleanup the renamed copy of the // original file if it exists, and flush the cache. if (_bFileExisted) { ::DeleteFile( szTempFileName ); } RevertUser( ); _cFilesReceived++; // Flush the atq cache here. TsFlushURL(QueryW3Instance()->GetTsvcCache(), _strURL.QueryStr(), _strURL.QueryCB(), RESERVED_DEMUX_URI_INFO); // // Now see if the mime type we currently have // matches the mimetype the client requested. If // it does, or the client requested one, we're // good. Otherwise we need to create a custom // mimetype entry for this file. // VerifyMimeType(); // Build and send the response. if ( !BuildPutDeleteHeader( _bFileExisted ? WTYPE_WRITE : WTYPE_CREATE )) { return FALSE; } SetState( HTR_DONE, _bFileExisted ? HT_OK : HT_CREATED, NO_ERROR ); return SendHeader( QueryRespBufPtr(), (DWORD) -1, IO_FLAG_ASYNC, pfFinished ); } else { // The rename failed. There's no easy way to tell why. // Leave a backup copy around, and log it so that an admin // can restore it. const CHAR *pszFileName[3]; err = GetLastError(); pszFileName[0] = _strRawURL.QueryStr(); pszFileName[1] = _strPhysicalPath.QueryStr(); if (_bFileExisted) { pszFileName[2] = szTempFileName; g_pInetSvc->LogEvent( W3_EVENT_CANNOT_PUT_RENAME, 3, pszFileName, 0 ); } else { g_pInetSvc->LogEvent( W3_EVENT_CANNOT_PUT, 2, pszFileName, 0 ); } IF_DEBUG(ERROR) { DBGPRINTF((DBG_CONTEXT, "MoveFileEx[%s to %s] failed with %d\n", _strTempFileName.QueryStr(), _strPhysicalPath.QueryStr(), err)); } ::DeleteFile( _strTempFileName.QueryStr( )); RevertUser( ); // // Rename failed somehow. // SetLastError(err); return FALSE; } } } while ( fDone ); return TRUE; break; case PSTATE_DISCARD_READ: // Just had a read complete. See if we've read all of the data for // this chunk. If not, wait until the next read completes. Otherwise // see if we're done reading and discarding. if ( !ReadEntityBody( &fDone, FALSE, QueryMetaData()->QueryPutReadSize() )) { return FALSE; } if (!fDone) { // Another read pending, so wait for it to complete. return TRUE; } // Otherwise we're read one chunk. Fall through to the discard chunk // code, to see if we're done overall and possibly get another // read going. case PSTATE_DISCARD_CHUNK: do { if ( QueryClientContentLength() <= QueryTotalEntityBodyCB() ) { // We're read all of the data, so we're done. SetState( HTR_DONE, _dwLogHttpResponse, NO_ERROR ); *pfFinished = TRUE; return TRUE; } // Haven't read it all yet, get another read going. _putstate = PSTATE_DISCARD_READ; _cbEntityBody = 0; _cbBytesWritten = 0; DBG_ASSERT(QueryClientContentLength() > QueryTotalEntityBodyCB()); if ( !ReadEntityBody( &fDone, TRUE, QueryMetaData()->QueryPutReadSize() )) { return FALSE; } } while ( fDone ); return TRUE; default: break; } return FALSE; } BOOL HTTP_REQUEST::DoDelete( BOOL * pfFinished ) /*++ Routine Description: Handle a DELETE request ( cf HTTP 1.1 spec ) Check the URL being deleted, make sure we have permission, and delete it. In order to be safe we need to be sure we can both delete the file and send a response header. To make this work we'll open the file with delete access. If that works we'll try and build and send the response header, and iff that works we'll actually try to delete the file. Arguments: None Return Value: TRUE if success, else FALSE --*/ { HANDLE hFile; SECURITY_ATTRIBUTES sa; DWORD err; STR ResponseStr; BOOL fDone; BOOL fReturn; BOOL fDeleted = FALSE; // // Make sure the virtual root allows write access // if ( !IS_ACCESS_ALLOWED(WRITE) ) { SetState( HTR_DONE, HT_FORBIDDEN, ERROR_ACCESS_DENIED ); Disconnect( HT_FORBIDDEN, IDS_WRITE_ACCESS_DENIED, FALSE, pfFinished ); return TRUE; } // Check that we're not trying to DELETE the '*' URL. if (*_strURL.QueryStr() == '*') { // Don't allow GETs on the server URL. SetState( HTR_DONE, HT_BAD_REQUEST, ERROR_INVALID_PARAMETER ); Disconnect( HT_BAD_REQUEST, NULL, FALSE, pfFinished ); return TRUE; } // Now get the filename lock. _pFileNameLock = AcquireFileNameLock(&_strPhysicalPath); if (_pFileNameLock == NULL) { // Couldn't get it, return 'resource busy'. return FALSE; } if ( !ImpersonateUser( ) ) { _pFileNameLock->Release(); _pFileNameLock = NULL; return (FALSE); } if ( TsDeleteOnClose(_pURIInfo, QueryImpersonationHandle(), &fDeleted ) ) { if ( !fDeleted ) { // A 'bad' error has occured, so return FALSE. RevertUser(); SetDeniedFlags( SF_DENIED_RESOURCE ); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } else { // Try to build and send the HTTP response header. if (!BuildPutDeleteHeader(WTYPE_DELETE)) { RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } } } if (!fDeleted) { sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = FALSE; hFile = ::CreateFile( _strPhysicalPath.QueryStr(), GENERIC_READ | GENERIC_WRITE, g_fIsWindows95 ? (FILE_SHARE_READ | FILE_SHARE_WRITE) : (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if ( hFile == INVALID_HANDLE_VALUE) { // Some sort of error opening the file. err = ::GetLastError(); RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; if ( err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) { // The path or filename itself is bad, fail the request. SetState( HTR_DONE, HT_NOT_FOUND, GetLastError() ); Disconnect( HT_NOT_FOUND, NO_ERROR, FALSE, pfFinished ); return TRUE; } if ( err == ERROR_INVALID_NAME ) { // An invalid name. SetState( HTR_DONE, HT_BAD_REQUEST, GetLastError() ); Disconnect( HT_BAD_REQUEST, NO_ERROR, FALSE, pfFinished ); return TRUE; } else { // A 'bad' error has occured, so return FALSE. if ( err == ERROR_ACCESS_DENIED ) { SetDeniedFlags( SF_DENIED_RESOURCE ); } } return FALSE; } else { // We should be able to delete it, so just close the handle. if (_fIfModifier) { fDone = CheckPreconditions(hFile, TRUE, pfFinished, &fReturn ); } else { fDone = FALSE; } ::CloseHandle(hFile); if (fDone) { RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; *pfFinished = TRUE; return fReturn; } } TsFlushURL(QueryW3Instance()->GetTsvcCache(), _strURL.QueryStr(), _strURL.QueryCB(), RESERVED_DEMUX_URI_INFO); // Try to build and send the HTTP response header. if (!BuildPutDeleteHeader(WTYPE_DELETE)) { RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; return FALSE; } // We built the successful response, now delete the actual file. fDeleted = ::DeleteFile( _strPhysicalPath.QueryStr() ); } RevertUser(); _pFileNameLock->Release(); _pFileNameLock = NULL; if ( !fDeleted) { return FALSE; } SetState( HTR_DONE, HT_OK, NO_ERROR ); if ( !SendHeader( QueryRespBufPtr(), (DWORD) -1, IO_FLAG_ASYNC, pfFinished )) { // Presumably if the WriteFile fails something serious has gone wrong, // and the connection is about to die. return FALSE; } // Here we should go ahead and purge this from the Tsunami cache return TRUE; } BOOL W95MoveFileEx( IN LPCSTR lpExistingFile, IN LPCSTR lpNewFile ) { BOOL fRet; fRet = ::CopyFile( lpExistingFile, lpNewFile, FALSE ); if ( fRet ) { ::DeleteFile(lpExistingFile); } else { IF_DEBUG(ERROR) { DBGPRINTF((DBG_CONTEXT, "Error %d in CopyFile[%s to %s]\n", GetLastError(), lpExistingFile, lpNewFile)); } } return(fRet); } // W95MoveFileEx VOID HTTP_REQUEST::CleanupWriteState( VOID ) /*++ Routine Description: Cleanup any of the write methods (PUT, DELETE) state when a request is completed. Arguments: None Return Value: None. --*/ { if (_pFileNameLock != NULL) { _pFileNameLock->Release(); _pFileNameLock = NULL; } if (_hTempFileHandle != INVALID_HANDLE_VALUE) { CleanupTempFile(); } } DWORD InitializeWriteState( VOID ) /*++ Routine Description: Initialize the global state we need in order to do write methods such as PUT and DELETE. Arguments: None Return Value: NO_ERROR on success, or error code if we fail. --*/ { DWORD i; for (i = 0; i < MAX_FN_LOCK_BUCKETS; i++) { InitializeListHead(&FNLockTable[i].ListHead); INITIALIZE_CRITICAL_SECTION(&FNLockTable[i].csCritSec); } if ( !InetIsNtServer( IISGetPlatformType() ) ) { dwPutNumCPU = 1; } else { SYSTEM_INFO si; // // get the count of CPUs for Thread Tuning. // GetSystemInfo( &si ); dwPutNumCPU = si.dwNumberOfProcessors; } dwPutBlockedCount = 0; return NO_ERROR; } VOID TerminateWriteState( VOID ) /*++ Routine Description: Terminate the global state we need in order to do write methods such as PUT and DELETE. Arguments: None Return Value: None --*/ { DWORD i; for (i = 0; i < MAX_FN_LOCK_BUCKETS; i++) { DeleteCriticalSection(&FNLockTable[i].csCritSec); } } BOOL HTTP_REQUEST::DoOptions( BOOL * pfFinished ) /*++ Routine Description: Handle an OPTIONS request. If this is a request for the '*' URL, we'll send back information about the global methods we support. If this is for a particular URL we'll send back information about what's allowable on that URL. Arguments: pfFinished - Set to TRUE if no further processings is needed for this request Return Value: TRUE if success, else FALSE --*/ { CHAR *pszResp; CHAR *pszTail; DWORD cbRespBytes; DWORD cbRespBytesNeeded; BOOL fDav = ((W3_IIS_SERVICE *) QueryW3Instance()->m_Service)->FDavDll(); // Build the basic response header first. if (!BuildBaseResponseHeader(QueryRespBuf(), pfFinished, NULL, 0)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); cbRespBytes = strlen(pszResp); cbRespBytesNeeded = cbRespBytes + sizeof("Public: OPTIONS, TRACE, GET, HEAD, POST, PUT, DELETE\r\n") - 1 + sizeof("Content-Length: 0\r\n\r\n"); if (!QueryRespBuf()->Resize(cbRespBytesNeeded)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); pszTail = pszResp + cbRespBytes; APPEND_STRING(pszTail, "Public: OPTIONS, TRACE, GET, HEAD, POST, PUT, DELETE\r\n"); if (*_strURL.QueryStr() != '*') { // We have an actual URL. Figure out what methods are applicable to it. cbRespBytes = DIFF(pszTail - pszResp); cbRespBytesNeeded = cbRespBytes + MAX_ALLOW_SIZE; if (!QueryRespBuf()->Resize(cbRespBytesNeeded)) { return FALSE; } pszResp = (CHAR *)QueryRespBufPtr(); pszTail = pszResp + cbRespBytes; pszTail += BuildAllowHeader(_strURL.QueryStr(), pszTail); } APPEND_STRING(pszTail, "Content-Length: 0\r\n\r\n"); SetState( HTR_DONE, HT_OK, NO_ERROR ); if ( !SendHeader( QueryRespBufPtr(), (DWORD) -1, IO_FLAG_ASYNC, pfFinished )) { return FALSE; } return TRUE; }