/* Copyright (C) 2013 Red Hat, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #define __STDC_FORMAT_MACROS #define __USE_MINGW_ANSI_STDIO 1 // compiler specific definitions #ifdef _MSC_VER // compiling with Visual Studio #define PRIu64 "I64u" #else // compiling with mingw #include #endif // compiler specific definitions #include #include #include "file_xfer.h" #include "as_user.h" #include "shell.h" #define FILENAME_RESERVED_CHAR_LIST \ ":" /* streams and devices */ \ "/\\" /* components separator */ \ "?*" /* wildcards */ \ "<>\"|" /* reserved to shell */ void FileXfer::reset() { _tasks.clear(); } FileXfer::~FileXfer() { } typedef HRESULT WINAPI SHGetKnownFolderPath_type(REFKNOWNFOLDERID rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath); static bool get_download_directory(TCHAR file_path[MAX_PATH]) { file_path[0] = 0; PWSTR path; HMODULE shell32 = GetModuleHandle(L"shell32.dll"); SHGetKnownFolderPath_type *SHGetKnownFolderPath_p = (SHGetKnownFolderPath_type *) GetProcAddress(shell32, "SHGetKnownFolderPath"); if (SHGetKnownFolderPath_p && SUCCEEDED(SHGetKnownFolderPath_p(FOLDERID_Downloads, 0, NULL, &path))) { if (_tcslen(path) < MAX_PATH) { _tcscpy(file_path, path); } CoTaskMemFree(path); } if (file_path[0] == 0 && FAILED(SHGetFolderPath(NULL, CSIDL_DESKTOPDIRECTORY | CSIDL_FLAG_CREATE, NULL, SHGFP_TYPE_CURRENT, file_path))) { vd_printf("failed getting desktop path"); return false; } return true; } void FileXfer::handle_start(VDAgentFileXferStartMessage* start, AgentFileXferStatusMessageFull& status, size_t& status_size) { char* file_meta = (char*)start->data; TCHAR file_path[MAX_PATH]; char file_name[MAX_PATH]; ULARGE_INTEGER free_bytes; uint64_t file_size; HANDLE handle; AsUser as_user; size_t wlen; status.common.id = start->id; status.common.result = VD_AGENT_FILE_XFER_STATUS_ERROR; if (!g_key_get_string(file_meta, "vdagent-file-xfer", "name", file_name, sizeof(file_name)) || !g_key_get_uint64(file_meta, "vdagent-file-xfer", "size", &file_size)) { vd_printf("file id %u meta parsing failed", start->id); return; } vd_printf("%u %s (%" PRIu64 ")", start->id, file_name, file_size); if (strcspn(file_name, FILENAME_RESERVED_CHAR_LIST) != strlen(file_name)) { status.common.result = VD_AGENT_FILE_XFER_STATUS_ERROR; status.error.error_type = VD_AGENT_FILE_XFER_STATUS_ERROR_GLIB_IO; status.error.error_code = 10; // G_IO_ERROR_INVALID_FILENAME status_size = sizeof(status.common) + sizeof(status.error); vd_printf("filename contains invalid characters"); return; } if (!as_user.begin()) { vd_printf("as_user failed"); return; } static_assert(SPICE_N_ELEMENTS(file_path) >= MAX_PATH, "file_path too small"); if (!get_download_directory(file_path)) { return; } if (!GetDiskFreeSpaceEx(file_path, &free_bytes, NULL, NULL)) { vd_printf("failed getting disk free space %lu", GetLastError()); return; } if (free_bytes.QuadPart < file_size) { status.common.result = VD_AGENT_FILE_XFER_STATUS_NOT_ENOUGH_SPACE; status.not_enough_space.disk_free_space = free_bytes.QuadPart; status_size = sizeof(status.common) + sizeof(status.not_enough_space); vd_printf("insufficient disk space %" PRIu64, free_bytes.QuadPart); return; } wlen = _tcslen(file_path); // make sure we have enough space // (1 char for separator, 1 char for filename and 1 char for NUL terminator) if (wlen + 3 >= SPICE_N_ELEMENTS(file_path)) { vd_printf("error: file too long %ls\\%s", file_path, file_name); return; } file_path[wlen++] = TEXT('\\'); file_path[wlen] = TEXT('\0'); const int MAX_ATTEMPTS = 64; // matches behavior of linux vdagent const size_t POSTFIX_LEN = 6; // up to 2 digits in parentheses and final NULL: " (xx)" char *extension = strrchr(file_name, '.'); if (!extension) extension = strchr(file_name, 0); for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { char dest_filename[SPICE_N_ELEMENTS(file_name) + POSTFIX_LEN]; if (attempt == 0) { strcpy(dest_filename, file_name); } else { snprintf(dest_filename, sizeof(dest_filename), "%.*s (%d)%s", int(extension - file_name), file_name, attempt, extension); } if ((MultiByteToWideChar(CP_UTF8, 0, dest_filename, -1, file_path + wlen, SPICE_N_ELEMENTS(file_path) - wlen)) == 0) { vd_printf("failed converting file_name:%s to WideChar", dest_filename); return; } handle = CreateFile(file_path, GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL); if (handle != INVALID_HANDLE_VALUE) { break; } // If the file already exists, we can re-try with a new filename. If // it's a different error, there's not much we can do. if (GetLastError() != ERROR_FILE_EXISTS) { vd_printf("Failed creating %ls %lu", file_path, GetLastError()); return; } } if (handle == INVALID_HANDLE_VALUE) { vd_printf("Failed creating %ls. More than 63 copies exist?", file_path); return; } auto task = std::make_shared(handle, file_size, file_path); _tasks[start->id] = task; status.common.result = VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA; } bool FileXfer::handle_data(VDAgentFileXferDataMessage* data, AgentFileXferStatusMessageFull& status, size_t& status_size) { FileXferTasks::iterator iter; DWORD written; status.common.id = data->id; status.common.result = VD_AGENT_FILE_XFER_STATUS_ERROR; iter = _tasks.find(data->id); if (iter == _tasks.end()) { vd_printf("file id %u not found", data->id); return true; } auto task = iter->second; task->pos += data->size; if (task->pos > task->size) { vd_printf("file xfer is longer than expected"); goto fin; } if (!WriteFile(task->handle, data->data, (DWORD)data->size, &written, NULL) || written != data->size) { vd_printf("file write failed %lu", GetLastError()); goto fin; } if (task->pos < task->size) { return false; } vd_printf("%u completed", iter->first); task->success(); status.common.result = VD_AGENT_FILE_XFER_STATUS_SUCCESS; fin: _tasks.erase(iter); return true; } FileXferTask::~FileXferTask() { if (handle != INVALID_HANDLE_VALUE) { CloseHandle(handle); DeleteFile(name); } } void FileXferTask::success() { // close the handle so the destructor won't delete the file if (handle != INVALID_HANDLE_VALUE) { CloseHandle(handle); handle = INVALID_HANDLE_VALUE; } // open download directory AsUser as_user; if (!as_user.begin()) { vd_printf("as_user failed"); return; } TCHAR file_path[MAX_PATH]; if (!get_download_directory(file_path)) { return; } open_shell_directory(file_path); } void FileXfer::handle_status(VDAgentFileXferStatusMessage* status) { FileXferTasks::iterator iter; vd_printf("id %u result %u", status->id, status->result); if (status->result != VD_AGENT_FILE_XFER_STATUS_CANCELLED) { vd_printf("only cancel is permitted"); return; } iter = _tasks.find(status->id); if (iter == _tasks.end()) { vd_printf("file id %u not found", status->id); return; } _tasks.erase(iter); } bool FileXfer::dispatch(VDAgentMessage* msg, AgentFileXferStatusMessageFull& status, size_t& status_size) { bool ret = false; switch (msg->type) { case VD_AGENT_FILE_XFER_START: handle_start((VDAgentFileXferStartMessage*)msg->data, status, status_size); ret = true; break; case VD_AGENT_FILE_XFER_DATA: ret = handle_data((VDAgentFileXferDataMessage*)msg->data, status, status_size); break; case VD_AGENT_FILE_XFER_STATUS: handle_status((VDAgentFileXferStatusMessage*)msg->data); break; default: vd_printf("unsupported message type %u size %u", msg->type, msg->size); } return ret; } //minimal parsers for GKeyFile, supporting only key=value with no spaces. #define G_KEY_MAX_LEN 256 bool FileXfer::g_key_get_string(char* data, const char* group, const char* key, char* value, unsigned vsize) { char group_pfx[G_KEY_MAX_LEN], key_pfx[G_KEY_MAX_LEN]; char *group_pos, *key_pos, *next_group_pos, *start, *end; size_t len; snprintf(group_pfx, sizeof(group_pfx), "[%s]", group); if (!(group_pos = strstr((char*)data, group_pfx))) return false; snprintf(key_pfx, sizeof(key_pfx), "\n%s=", key); if (!(key_pos = strstr(group_pos, key_pfx))) return false; next_group_pos = strstr(group_pos + strlen(group_pfx), "\n["); if (next_group_pos && key_pos > next_group_pos) return false; start = key_pos + strlen(key_pfx); end = strchr(start, '\n'); if (!end) return false; len = end - start; if (len >= vsize) return false; memcpy(value, start, len); value[len] = '\0'; return true; } bool FileXfer::g_key_get_uint64(char* data, const char* group, const char* key, uint64_t* value) { char str[G_KEY_MAX_LEN]; if (!g_key_get_string(data, group, key, str, sizeof(str))) return false; return !!sscanf(str, "%" PRIu64, value); }