From d8e131ad08df56c5d6d82dbc4050bd0d4fe5b4ed Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2016 11:01:17 +0530 Subject: [PATCH] Use authentication for the named pipe in the windows file dialogs --- setup/installer/windows/file_dialogs.cpp | 251 ++++++++++++----------- src/calibre/gui2/win_file_dialogs.py | 27 ++- 2 files changed, 150 insertions(+), 128 deletions(-) diff --git a/setup/installer/windows/file_dialogs.cpp b/setup/installer/windows/file_dialogs.cpp index 066d37b08a..85ae307d79 100644 --- a/setup/installer/windows/file_dialogs.cpp +++ b/setup/installer/windows/file_dialogs.cpp @@ -17,43 +17,44 @@ #include #define PRINTERR(x) fprintf(stderr, "%s", x); fflush(stderr); +#define SECRET_SIZE 32 bool write_bytes(HANDLE pipe, DWORD sz, const char* buf) { - DWORD written = 0; - if (!WriteFile(pipe, buf, sz, &written, NULL)) { - fprintf(stderr, "Failed to write to pipe. GetLastError()=%d\n", GetLastError()); fflush(stderr); return false; - } - if (written != sz) { - fprintf(stderr, "Failed to write to pipe. Incomplete write, leftover bytes: %d", sz - written); fflush(stderr); return false; - } - return true; + DWORD written = 0; + if (!WriteFile(pipe, buf, sz, &written, NULL)) { + fprintf(stderr, "Failed to write to pipe. GetLastError()=%d\n", GetLastError()); fflush(stderr); return false; + } + if (written != sz) { + fprintf(stderr, "Failed to write to pipe. Incomplete write, leftover bytes: %d", sz - written); fflush(stderr); return false; + } + return true; } bool read_bytes(size_t sz, char* buf, bool allow_incomplete=false) { - char *ptr = buf, *limit = buf + sz; - while(limit > ptr && !feof(stdin) && !ferror(stdin)) { - ptr += fread(ptr, 1, limit - ptr, stdin); - } - if (ferror(stdin)) { PRINTERR("Failed to read from stdin!"); return false; } - if (ptr - buf != sz) { if (!allow_incomplete) PRINTERR("Truncated input!"); return false; } - return true; + char *ptr = buf, *limit = buf + sz; + while(limit > ptr && !feof(stdin) && !ferror(stdin)) { + ptr += fread(ptr, 1, limit - ptr, stdin); + } + if (ferror(stdin)) { PRINTERR("Failed to read from stdin!"); return false; } + if (ptr - buf != sz) { if (!allow_incomplete) PRINTERR("Truncated input!"); return false; } + return true; } bool from_utf8(size_t sz, const char *src, LPWSTR* ans) { - int asz = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, (int)sz, NULL, 0); - if (!asz) { PRINTERR("Failed to get size of UTF-8 string"); return false; } - *ans = (LPWSTR)calloc(asz+1, sizeof(wchar_t)); - if(*ans == NULL) { PRINTERR("Out of memory!"); return false; } - asz = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, (int)sz, *ans, asz); - if (!asz) { PRINTERR("Failed to convert UTF-8 string"); return false; } - return true; + int asz = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, (int)sz, NULL, 0); + if (!asz) { PRINTERR("Failed to get size of UTF-8 string"); return false; } + *ans = (LPWSTR)calloc(asz+1, sizeof(wchar_t)); + if(*ans == NULL) { PRINTERR("Out of memory!"); return false; } + asz = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, (int)sz, *ans, asz); + if (!asz) { PRINTERR("Failed to convert UTF-8 string"); return false; } + return true; } char* to_utf8(LPCWSTR src, int *sz) { - // Convert to a null-terminated UTF-8 encoded bytearray, allocated on the heap + // Convert to a null-terminated UTF-8 encoded bytearray, allocated on the heap char *ans = NULL; *sz = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, NULL, 0, NULL, NULL); - if (!*sz) { PRINTERR("Failed to get size of UTF-16 string"); return NULL; } + if (!*sz) { PRINTERR("Failed to get size of UTF-16 string"); return NULL; } ans = (char*)calloc((*sz) + 1, sizeof(char)); if (ans == NULL) { PRINTERR("Out of memory!"); return NULL; } *sz = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, ans, *sz, NULL, NULL); @@ -64,10 +65,10 @@ char* to_utf8(LPCWSTR src, int *sz) { static char* rsbuf = NULL; bool read_string(unsigned short sz, LPWSTR* ans) { - memset(rsbuf, 0, 65537); - if (!read_bytes(sz, rsbuf)) return false; - if (!from_utf8(sz, rsbuf, ans)) return false; - return true; + memset(rsbuf, 0, 65537); + if (!read_bytes(sz, rsbuf)) return false; + if (!from_utf8(sz, rsbuf, ans)) return false; + return true; } COMDLG_FILTERSPEC *read_file_types(UINT *num_file_types) { @@ -102,52 +103,52 @@ static void print_com_error(HRESULT hr, const char *msg) { #define REPORTERR(hr, x) { print_com_error(hr, x); ret = 1; goto error; } #define CALLCOM(x, err) hr = x; if(FAILED(hr)) REPORTERR(hr, err) -int show_dialog(HANDLE pipe, HWND parent, bool save_dialog, LPWSTR title, LPWSTR folder, LPWSTR filename, LPWSTR save_path, bool multiselect, bool confirm_overwrite, bool only_dirs, bool no_symlinks, COMDLG_FILTERSPEC *file_types, UINT num_file_types) { - int ret = 0, name_sz = 0; - IFileDialog *pfd = NULL; - IShellItemArray *items = NULL; +int show_dialog(HANDLE pipe, char *secret, HWND parent, bool save_dialog, LPWSTR title, LPWSTR folder, LPWSTR filename, LPWSTR save_path, bool multiselect, bool confirm_overwrite, bool only_dirs, bool no_symlinks, COMDLG_FILTERSPEC *file_types, UINT num_file_types) { + int ret = 0, name_sz = 0; + IFileDialog *pfd = NULL; + IShellItemArray *items = NULL; IShellItem *item = NULL, *folder_item = NULL, *save_path_item = NULL; char *path = NULL; - DWORD options = 0, item_count = 0; + DWORD options = 0, item_count = 0; LPWSTR name = NULL; - HRESULT hr = S_OK; - hr = CoInitialize(NULL); - if (FAILED(hr)) { PRINTERR("Failed to initialize COM"); return 1; } + HRESULT hr = S_OK; + hr = CoInitialize(NULL); + if (FAILED(hr)) { PRINTERR("Failed to initialize COM"); return 1; } - CALLCOM(CoCreateInstance((save_dialog ? CLSID_FileSaveDialog : CLSID_FileOpenDialog), - NULL, CLSCTX_INPROC_SERVER, (save_dialog ? IID_IFileSaveDialog : IID_IFileOpenDialog), - reinterpret_cast(&pfd)), - "Failed to create COM object for file dialog") - CALLCOM(pfd->GetOptions(&options), "Failed to get options") - options |= FOS_PATHMUSTEXIST | FOS_FORCESHOWHIDDEN; - if (no_symlinks) options |= FOS_NODEREFERENCELINKS; - if (save_dialog) { - options |= FOS_NOREADONLYRETURN; - if (confirm_overwrite) options |= FOS_OVERWRITEPROMPT; - if (save_path != NULL) { - hr = SHCreateItemFromParsingName(save_path, NULL, IID_IShellItem, reinterpret_cast(&save_path_item)); - // Failure to set initial save path is not critical - if (SUCCEEDED(hr)) ((IFileSaveDialog*)pfd)->SetSaveAsItem(save_path_item); - } - } else { - if (multiselect) options |= FOS_ALLOWMULTISELECT; - if (only_dirs) options |= FOS_PICKFOLDERS; - options |= FOS_FILEMUSTEXIST; - } - CALLCOM(pfd->SetOptions(options), "Failed to set options") - if (title != NULL) { CALLCOM(pfd->SetTitle(title), "Failed to set title") } - if (folder != NULL) { - hr = SHCreateItemFromParsingName(folder, NULL, IID_IShellItem, reinterpret_cast(&folder_item)); - // Failure to set initial folder is not critical - if (SUCCEEDED(hr)) pfd->SetFolder(folder_item); - } - if (filename != NULL) pfd->SetFileName(filename); // Failure is not critical + CALLCOM(CoCreateInstance((save_dialog ? CLSID_FileSaveDialog : CLSID_FileOpenDialog), + NULL, CLSCTX_INPROC_SERVER, (save_dialog ? IID_IFileSaveDialog : IID_IFileOpenDialog), + reinterpret_cast(&pfd)), + "Failed to create COM object for file dialog") + CALLCOM(pfd->GetOptions(&options), "Failed to get options") + options |= FOS_PATHMUSTEXIST | FOS_FORCESHOWHIDDEN; + if (no_symlinks) options |= FOS_NODEREFERENCELINKS; + if (save_dialog) { + options |= FOS_NOREADONLYRETURN; + if (confirm_overwrite) options |= FOS_OVERWRITEPROMPT; + if (save_path != NULL) { + hr = SHCreateItemFromParsingName(save_path, NULL, IID_IShellItem, reinterpret_cast(&save_path_item)); + // Failure to set initial save path is not critical + if (SUCCEEDED(hr)) ((IFileSaveDialog*)pfd)->SetSaveAsItem(save_path_item); + } + } else { + if (multiselect) options |= FOS_ALLOWMULTISELECT; + if (only_dirs) options |= FOS_PICKFOLDERS; + options |= FOS_FILEMUSTEXIST; + } + CALLCOM(pfd->SetOptions(options), "Failed to set options") + if (title != NULL) { CALLCOM(pfd->SetTitle(title), "Failed to set title") } + if (folder != NULL) { + hr = SHCreateItemFromParsingName(folder, NULL, IID_IShellItem, reinterpret_cast(&folder_item)); + // Failure to set initial folder is not critical + if (SUCCEEDED(hr)) pfd->SetFolder(folder_item); + } + if (filename != NULL) pfd->SetFileName(filename); // Failure is not critical if (!(options & FOS_PICKFOLDERS) && file_types != NULL && num_file_types > 0) { CALLCOM(pfd->SetFileTypes(num_file_types, file_types), "Failed to set file types") } - hr = pfd->Show(parent); - if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) goto error; - if (FAILED(hr)) REPORTERR(hr, "Failed to show dialog") + hr = pfd->Show(parent); + if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) goto error; + if (FAILED(hr)) REPORTERR(hr, "Failed to show dialog") if (save_dialog) { CALLCOM(pfd->GetResult(&item), "Failed to get save dialog result"); @@ -155,11 +156,13 @@ int show_dialog(HANDLE pipe, HWND parent, bool save_dialog, LPWSTR title, LPWSTR path = to_utf8(name, &name_sz); CoTaskMemFree(name); name = NULL; if (path == NULL) return 1; + if (!write_bytes(pipe, SECRET_SIZE+1, secret)) return 1; if (!write_bytes(pipe, name_sz, path)) return 1; } else { CALLCOM(((IFileOpenDialog*)pfd)->GetResults(&items), "Failed to get dialog results"); CALLCOM(items->GetCount(&item_count), "Failed to get count of results"); if (item_count > 0) { + if (!write_bytes(pipe, SECRET_SIZE+1, secret)) return 1; for (DWORD i = 0; i < item_count; i++) { CALLCOM(items->GetItemAt(i, &item), "Failed to get result item"); if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &name))) { @@ -173,9 +176,9 @@ int show_dialog(HANDLE pipe, HWND parent, bool save_dialog, LPWSTR title, LPWSTR } error: - if(pfd) pfd->Release(); - CoUninitialize(); - return ret; + if(pfd) pfd->Release(); + CoUninitialize(); + return ret; } #define READ(x, y) if (!read_bytes((x), (y))) return 1; #define CHECK_KEY(x) (key_size == sizeof(x) - 1 && memcmp(buf, x, sizeof(x) - 1) == 0) @@ -184,87 +187,91 @@ error: #define READBOOL(x) READ(1, buf); x = !!buf[0]; HANDLE open_named_pipe(LPWSTR pipename) { - HANDLE ans = INVALID_HANDLE_VALUE; - while(true) { - ans = CreateFileW(pipename, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); - if (ans != INVALID_HANDLE_VALUE) break; - if (GetLastError() != ERROR_PIPE_BUSY) { - fprintf(stderr, "Failed to open pipe. GetLastError()=%d\n", GetLastError()); fflush(stderr); return ans; - } - if (!WaitNamedPipeW(pipename, 20000)) { - fprintf(stderr, "Failed to open pipe. 20 second wait timed out.\n", GetLastError()); fflush(stderr); return ans; - } - } - return ans; + HANDLE ans = INVALID_HANDLE_VALUE; + while(true) { + ans = CreateFileW(pipename, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (ans != INVALID_HANDLE_VALUE) break; + if (GetLastError() != ERROR_PIPE_BUSY) { + fprintf(stderr, "Failed to open pipe. GetLastError()=%d\n", GetLastError()); fflush(stderr); return ans; + } + if (!WaitNamedPipeW(pipename, 20000)) { + fprintf(stderr, "Failed to open pipe. 20 second wait timed out.\n", GetLastError()); fflush(stderr); return ans; + } + } + return ans; } int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { - char buf[257] = {0}; - size_t key_size = 0; - HWND parent = NULL; - bool save_dialog = false, multiselect = false, confirm_overwrite = false, only_dirs = false, no_symlinks = false; - unsigned short len = 0; - LPWSTR title = NULL, folder = NULL, filename = NULL, save_path = NULL, echo = NULL, pipename = NULL; + char buf[257] = {0}, secret[SECRET_SIZE + 1] = {0}; + size_t key_size = 0; + HWND parent = NULL; + bool save_dialog = false, multiselect = false, confirm_overwrite = false, only_dirs = false, no_symlinks = false; + unsigned short len = 0; + LPWSTR title = NULL, folder = NULL, filename = NULL, save_path = NULL, echo = NULL, pipename = NULL; COMDLG_FILTERSPEC *file_types = NULL; UINT num_file_types = 0; - HANDLE pipe = INVALID_HANDLE_VALUE; + HANDLE pipe = INVALID_HANDLE_VALUE; - SETBINARY(stdout); SETBINARY(stdin); SETBINARY(stderr); - // The calibre executables call SetDllDirectory, we unset it here just in - // case it interferes with some idiotic shell extension or the other - SetDllDirectory(NULL); + SETBINARY(stdout); SETBINARY(stdin); SETBINARY(stderr); + // The calibre executables call SetDllDirectory, we unset it here just in + // case it interferes with some idiotic shell extension or the other + SetDllDirectory(NULL); rsbuf = (char*)calloc(65537, sizeof(char)); if(rsbuf == NULL) { PRINTERR("Out of memory!"); return 1; } - while(!feof(stdin)) { - memset(buf, 0, sizeof(buf)); - if(!read_bytes(1, buf, true)) { if (feof(stdin)) break; return 1;} - key_size = (size_t)buf[0]; - READ(key_size, buf); - if CHECK_KEY("HWND") { - READ(sizeof(HWND), buf); - if (sizeof(HWND) == 8) parent = (HWND)*((__int64*)buf); - else if (sizeof(HWND) == 4) parent = (HWND)*((__int32*)buf); - else { fprintf(stderr, "Unknown pointer size: %zd", sizeof(HWND)); fflush(stderr); return 1;} - } + while(!feof(stdin)) { + memset(buf, 0, sizeof(buf)); + if(!read_bytes(1, buf, true)) { if (feof(stdin)) break; return 1;} + key_size = (size_t)buf[0]; + READ(key_size, buf); + if CHECK_KEY("HWND") { + READ(sizeof(HWND), buf); + if (sizeof(HWND) == 8) parent = (HWND)*((__int64*)buf); + else if (sizeof(HWND) == 4) parent = (HWND)*((__int32*)buf); + else { fprintf(stderr, "Unknown pointer size: %zd", sizeof(HWND)); fflush(stderr); return 1;} + } - else if CHECK_KEY("PIPENAME") { READSTR(pipename); pipe = open_named_pipe(pipename); if (pipe == INVALID_HANDLE_VALUE) return 1; } + else if CHECK_KEY("PIPENAME") { READSTR(pipename); pipe = open_named_pipe(pipename); if (pipe == INVALID_HANDLE_VALUE) return 1; } - else if CHECK_KEY("TITLE") { READSTR(title) } + else if CHECK_KEY("SECRET") { if(!read_bytes(SECRET_SIZE, secret)) return 1; } - else if CHECK_KEY("FOLDER") { READSTR(folder) } + else if CHECK_KEY("TITLE") { READSTR(title) } - else if CHECK_KEY("FILENAME") { READSTR(filename) } + else if CHECK_KEY("FOLDER") { READSTR(folder) } - else if CHECK_KEY("SAVE_PATH") { READSTR(save_path) } + else if CHECK_KEY("FILENAME") { READSTR(filename) } - else if CHECK_KEY("SAVE_AS") { READBOOL(save_dialog) } + else if CHECK_KEY("SAVE_PATH") { READSTR(save_path) } - else if CHECK_KEY("MULTISELECT") { READBOOL(multiselect) } + else if CHECK_KEY("SAVE_AS") { READBOOL(save_dialog) } - else if CHECK_KEY("CONFIRM_OVERWRITE") { READBOOL(confirm_overwrite) } + else if CHECK_KEY("MULTISELECT") { READBOOL(multiselect) } - else if CHECK_KEY("ONLY_DIRS") { READBOOL(only_dirs) } + else if CHECK_KEY("CONFIRM_OVERWRITE") { READBOOL(confirm_overwrite) } - else if CHECK_KEY("NO_SYMLINKS") { READBOOL(no_symlinks) } + else if CHECK_KEY("ONLY_DIRS") { READBOOL(only_dirs) } + + else if CHECK_KEY("NO_SYMLINKS") { READBOOL(no_symlinks) } else if CHECK_KEY("FILE_TYPES") { file_types = read_file_types(&num_file_types); if (file_types == NULL) return 1; } else if CHECK_KEY("ECHO") { READSTR(echo) } - else { - PRINTERR("Unknown key"); - return 1; - } - } + else { + PRINTERR("Unknown key"); + return 1; + } + } - if (pipe == INVALID_HANDLE_VALUE) { PRINTERR("No pipename received"); return 1; } + if (pipe == INVALID_HANDLE_VALUE) { PRINTERR("No pipename received"); return 1; } + if (secret == NULL) { PRINTERR("No secret received"); return 1; } if (echo != NULL) { int echo_sz = 0; char *echo_buf = to_utf8(echo, &echo_sz); - return write_bytes(pipe, echo_sz, echo_buf) ? 0 : 1; + if (!write_bytes(pipe, SECRET_SIZE+1, secret)) return 1; + return write_bytes(pipe, echo_sz, echo_buf) ? 0 : 1; } - return show_dialog(pipe, parent, save_dialog, title, folder, filename, save_path, multiselect, confirm_overwrite, only_dirs, no_symlinks, file_types, num_file_types); + return show_dialog(pipe, secret, parent, save_dialog, title, folder, filename, save_path, multiselect, confirm_overwrite, only_dirs, no_symlinks, file_types, num_file_types); } diff --git a/src/calibre/gui2/win_file_dialogs.py b/src/calibre/gui2/win_file_dialogs.py index 10e58b5d95..fa06b24251 100644 --- a/src/calibre/gui2/win_file_dialogs.py +++ b/src/calibre/gui2/win_file_dialogs.py @@ -39,6 +39,9 @@ def serialize_hwnd(hwnd): return b'' return struct.pack(b'=B4s' + (b'Q' if is64bit else b'I'), 4, b'HWND', int(hwnd)) +def serialize_secret(secret): + return struct.pack(b'=B6s32s', 6, b'SECRET', secret) + def serialize_binary(key, val): key = key.encode('ascii') if not isinstance(key, bytes) else key return struct.pack(b'=B%ssB' % len(key), len(key), key, int(val)) @@ -110,8 +113,9 @@ def run_file_dialog( from calibre.gui2 import sanitize_env_vars with sanitize_env_vars(): env = os.environ.copy() + secret = os.urandom(32) pipename = '\\\\.\\pipe\\%s' % uuid4() - data = [serialize_string('PIPENAME', pipename)] + data = [serialize_string('PIPENAME', pipename), serialize_secret(secret)] parent = parent or None if parent is not None: data.append(serialize_hwnd(get_hwnd(parent))) @@ -165,9 +169,11 @@ def run_file_dialog( except Exception: x = repr(x) return x + def get_errors(): + return decode(h.stdoutdata) + ' ' + decode(h.stderrdata) if h.rc != 0: - raise Exception('File dialog failed: ' + decode(h.stdoutdata) + ' ' + decode(h.stderrdata)) + raise Exception('File dialog failed: ' + get_errors()) server.join(2) if server.is_alive(): raise Exception('Timed out waiting for read from pipe to complete') @@ -175,7 +181,12 @@ def run_file_dialog( raise Exception(server.err_msg) if not server.data: return () - ans = tuple((os.path.abspath(x.decode('utf-8')) for x in server.data.split(b'\0') if x)) + parts = list(filter(None, server.data.split(b'\0'))) + if len(parts) < 2: + return () + if parts[0] != secret: + raise Exception('File dialog failed, incorrect secret received: ' + get_errors()) + ans = tuple((os.path.abspath(x.decode('utf-8')) for x in parts[1:])) return ans def get_initial_folder(name, title, default_dir='~', no_save_dir=False): @@ -289,16 +300,20 @@ class PipeServer(Thread): def test(helper=HELPER): pipename = '\\\\.\\pipe\\%s' % uuid4() echo = '\U0001f431 Hello world!' - data = serialize_string('PIPENAME', pipename) + serialize_string('ECHO', echo) + secret = os.urandom(32) + data = serialize_string('PIPENAME', pipename) + serialize_string('ECHO', echo) + serialize_secret(secret) server = PipeServer(pipename) p = subprocess.Popen([helper], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate(data) if p.wait() != 0: - raise Exception('File dialog failed: ' + stderr.decode('utf-8')) + raise Exception('File dialog failed: ' + stdout.decode('utf-8') + ' ' + stderr.decode('utf-8')) if server.err_msg is not None: raise RuntimeError(server.err_msg) server.join(2) - q = server.data[:-1].decode('utf-8') + parts = filter(None, server.data.split(b'\0')) + if parts[0] != secret: + raise RuntimeError('Did not get back secret: %r != %r' % (secret, parts[0])) + q = parts[1].decode('utf-8') if q != echo: raise RuntimeError('Unexpected response: %r' % server.data)