// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/download/base_file.h" #include #include #include #include #include "base/files/file.h" #include "base/files/file_util.h" #include "base/guid.h" #include "base/metrics/histogram.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/thread_restrictions.h" #include "content/browser/download/download_interrupt_reasons_impl.h" #include "content/browser/download/download_stats.h" #include "content/browser/safe_util_win.h" #include "content/public/browser/browser_thread.h" namespace content { namespace { const int kAllSpecialShFileOperationCodes[] = { // Should be kept in sync with the case statement below. ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, ERROR_INVALID_PARAMETER, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x78, 0x79, 0x7A, 0x7C, 0x7D, 0x7E, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0xB7, 0x402, 0x10000, 0x10074, }; // Maps the result of a call to |SHFileOperation()| onto a // |DownloadInterruptReason|. // // These return codes are *old* (as in, DOS era), and specific to // |SHFileOperation()|. // They do not appear in any windows header. // // See http://msdn.microsoft.com/en-us/library/bb762164(VS.85).aspx. DownloadInterruptReason MapShFileOperationCodes(int code) { DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE; // Check these pre-Win32 error codes first, then check for matches // in Winerror.h. // This switch statement should be kept in sync with the list of codes // above. switch (code) { // Not a pre-Win32 error code; here so that this particular case shows up in // our histograms. Unfortunately, it is used not just to signal actual // ACCESS_DENIED errors, but many other errors as well. So we treat it as a // transient error. case ERROR_ACCESS_DENIED: // Access is denied. result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR; break; // This isn't documented but returned from SHFileOperation. Sharing // violations indicate that another process had the file open while we were // trying to rename. Anti-virus is believed to be the cause of this error in // the wild. Treated as a transient error on the assumption that the file // will be made available for renaming at a later time. case ERROR_SHARING_VIOLATION: result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR; break; // This is also not a documented return value of SHFileOperation, but has // been observed in the wild. We are treating it as a transient error based // on the cases we have seen so far. See http://crbug.com/368455. case ERROR_INVALID_PARAMETER: result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR; break; // The source and destination files are the same file. // DE_SAMEFILE == 0x71 case 0x71: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The operation was canceled by the user, or silently canceled if the // appropriate flags were supplied to SHFileOperation. // DE_OPCANCELLED == 0x75 case 0x75: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // Security settings denied access to the source. // DE_ACCESSDENIEDSRC == 0x78 case 0x78: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // The source or destination path exceeded or would exceed MAX_PATH. // DE_PATHTOODEEP == 0x79 case 0x79: result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG; break; // The path in the source or destination or both was invalid. // DE_INVALIDFILES == 0x7C case 0x7C: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The destination path is an existing file. // DE_FLDDESTISFILE == 0x7E case 0x7E: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The destination path is an existing folder. // DE_FILEDESTISFLD == 0x80 case 0x80: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The name of the file exceeds MAX_PATH. // DE_FILENAMETOOLONG == 0x81 case 0x81: result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG; break; // The destination is a read-only CD-ROM, possibly unformatted. // DE_DEST_IS_CDROM == 0x82 case 0x82: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // The destination is a read-only DVD, possibly unformatted. // DE_DEST_IS_DVD == 0x83 case 0x83: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // The destination is a writable CD-ROM, possibly unformatted. // DE_DEST_IS_CDRECORD == 0x84 case 0x84: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // The file involved in the operation is too large for the destination // media or file system. // DE_FILE_TOO_LARGE == 0x85 case 0x85: result = DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE; break; // The source is a read-only CD-ROM, possibly unformatted. // DE_SRC_IS_CDROM == 0x86 case 0x86: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // The source is a read-only DVD, possibly unformatted. // DE_SRC_IS_DVD == 0x87 case 0x87: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // The source is a writable CD-ROM, possibly unformatted. // DE_SRC_IS_CDRECORD == 0x88 case 0x88: result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED; break; // MAX_PATH was exceeded during the operation. // DE_ERROR_MAX == 0xB7 case 0xB7: result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG; break; // An unspecified error occurred on the destination. // XE_ERRORONDEST == 0x10000 case 0x10000: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // Multiple file paths were specified in the source buffer, but only one // destination file path. // DE_MANYSRC1DEST == 0x72 case 0x72: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // Rename operation was specified but the destination path is // a different directory. Use the move operation instead. // DE_DIFFDIR == 0x73 case 0x73: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The source is a root directory, which cannot be moved or renamed. // DE_ROOTDIR == 0x74 case 0x74: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The destination is a subtree of the source. // DE_DESTSUBTREE == 0x76 case 0x76: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The operation involved multiple destination paths, // which can fail in the case of a move operation. // DE_MANYDEST == 0x7A case 0x7A: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // The source and destination have the same parent folder. // DE_DESTSAMETREE == 0x7D case 0x7D: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // An unknown error occurred. This is typically due to an invalid path in // the source or destination. This error does not occur on Windows Vista // and later. // DE_UNKNOWN_ERROR == 0x402 case 0x402: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; // Destination is a root directory and cannot be renamed. // DE_ROOTDIR | ERRORONDEST == 0x10074 case 0x10074: result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; break; } // Narrow down on the reason we're getting some catch-all interrupt reasons. if (result == DOWNLOAD_INTERRUPT_REASON_FILE_FAILED) { UMA_HISTOGRAM_CUSTOM_ENUMERATION( "Download.MapWinShErrorFileFailed", code, base::CustomHistogram::ArrayToCustomRanges( kAllSpecialShFileOperationCodes, arraysize(kAllSpecialShFileOperationCodes))); } if (result == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED) { UMA_HISTOGRAM_CUSTOM_ENUMERATION( "Download.MapWinShErrorAccessDenied", code, base::CustomHistogram::ArrayToCustomRanges( kAllSpecialShFileOperationCodes, arraysize(kAllSpecialShFileOperationCodes))); } if (result == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR) { UMA_HISTOGRAM_CUSTOM_ENUMERATION( "Download.MapWinShErrorTransientError", code, base::CustomHistogram::ArrayToCustomRanges( kAllSpecialShFileOperationCodes, arraysize(kAllSpecialShFileOperationCodes))); } if (result != DOWNLOAD_INTERRUPT_REASON_NONE) return result; // If not one of the above codes, it should be a standard Windows error code. return ConvertFileErrorToInterruptReason( base::File::OSErrorToFileError(code)); } // Maps a return code from ScanAndSaveDownloadedFile() to a // DownloadInterruptReason. The return code in |result| is usually from the // final IAttachmentExecute::Save() call. DownloadInterruptReason MapScanAndSaveErrorCodeToInterruptReason( HRESULT result) { if (SUCCEEDED(result)) return DOWNLOAD_INTERRUPT_REASON_NONE; switch (result) { case INET_E_SECURITY_PROBLEM: // 0x800c000e // This is returned if the download was blocked due to security // restrictions. E.g. if the source URL was in the Restricted Sites zone // and downloads are blocked on that zone, then the download would be // deleted and this error code is returned. return DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED; case E_FAIL: // 0x80004005 // Returned if an anti-virus product reports an infection in the // downloaded file during IAE::Save(). return DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED; default: // Any other error that occurs during IAttachmentExecute::Save() likely // indicates a problem with the security check, but not necessarily the // download. See http://crbug.com/153212. return DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED; } } } // namespace // Renames a file using the SHFileOperation API to ensure that the target file // gets the correct default security descriptor in the new path. // Returns a network error, or net::OK for success. DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions( const base::FilePath& new_path) { base::ThreadRestrictions::AssertIOAllowed(); // The parameters to SHFileOperation must be terminated with 2 NULL chars. base::FilePath::StringType source = full_path_.value(); base::FilePath::StringType target = new_path.value(); source.append(1, L'\0'); target.append(1, L'\0'); SHFILEOPSTRUCT move_info = {0}; move_info.wFunc = FO_MOVE; move_info.pFrom = source.c_str(); move_info.pTo = target.c_str(); move_info.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS; base::TimeTicks now = base::TimeTicks::Now(); int result = SHFileOperation(&move_info); DownloadInterruptReason interrupt_reason = DOWNLOAD_INTERRUPT_REASON_NONE; if (result == 0 && move_info.fAnyOperationsAborted) interrupt_reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED; else if (result != 0) interrupt_reason = MapShFileOperationCodes(result); if (interrupt_reason != DOWNLOAD_INTERRUPT_REASON_NONE) return LogInterruptReason("SHFileOperation", result, interrupt_reason); return interrupt_reason; } DownloadInterruptReason BaseFile::AnnotateWithSourceInformation() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); DCHECK(!detached_); bound_net_log_.BeginEvent(net::NetLog::TYPE_DOWNLOAD_FILE_ANNOTATED); DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE; std::string braces_guid = "{" + client_guid_ + "}"; GUID guid = GUID_NULL; if (base::IsValidGUID(client_guid_)) { HRESULT hr = CLSIDFromString( base::UTF8ToUTF16(braces_guid).c_str(), &guid); if (FAILED(hr)) guid = GUID_NULL; } HRESULT hr = AVScanFile(full_path_, source_url_.spec(), guid); // If the download file is missing after the call, then treat this as an // interrupted download. // // If the ScanAndSaveDownloadedFile() call failed, but the downloaded file is // still around, then don't interrupt the download. Attachment Execution // Services deletes the submitted file if the downloaded file is blocked by // policy or if it was found to be infected. // // If the file is still there, then the error could be due to AES not being // available or some other error during the AES invocation. In either case, // we don't surface the error to the user. if (!base::PathExists(full_path_)) { DCHECK(FAILED(hr)); result = MapScanAndSaveErrorCodeToInterruptReason(hr); if (result == DOWNLOAD_INTERRUPT_REASON_NONE) { RecordDownloadCount(FILE_MISSING_AFTER_SUCCESSFUL_SCAN_COUNT); result = DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED; } LogInterruptReason("ScanAndSaveDownloadedFile", hr, result); } bound_net_log_.EndEvent(net::NetLog::TYPE_DOWNLOAD_FILE_ANNOTATED); return result; } } // namespace content