From c3c5c1fa7f292d8a9e40529b9eb8742ad20e21e5 Mon Sep 17 00:00:00 2001 From: Manoj Ampalam Date: Fri, 14 Apr 2017 16:15:32 -0700 Subject: [PATCH] Support I/O redirection in all ssh clients (ssh.exe, sftp.exe and scp.exe) (#113) PowerShell/Win32-OpenSSH#668 --- contrib/win32/openssh/config.h.vs | 2 +- contrib/win32/openssh/keygen.vcxproj | 5 +- contrib/win32/openssh/keygen.vcxproj.filters | 7 +- contrib/win32/openssh/scp.vcxproj | 5 +- contrib/win32/openssh/scp.vcxproj.filters | 7 +- contrib/win32/openssh/sftp.vcxproj | 5 +- contrib/win32/openssh/sftp.vcxproj.filters | 7 +- contrib/win32/openssh/ssh.vcxproj | 7 +- contrib/win32/openssh/ssh.vcxproj.filters | 7 +- contrib/win32/openssh/win32iocompat.vcxproj | 1 - .../openssh/win32iocompat.vcxproj.filters | 1 - contrib/win32/win32compat/fileio.c | 67 ++----- contrib/win32/win32compat/inc/fcntl.h | 25 ++- contrib/win32/win32compat/misc.c | 117 ------------- contrib/win32/win32compat/misc_internal.h | 3 + contrib/win32/win32compat/signal.c | 6 +- contrib/win32/win32compat/socketio.c | 2 +- contrib/win32/win32compat/termio.c | 79 ++++++--- contrib/win32/win32compat/w32fd.c | 163 ++++++++++++++++-- contrib/win32/win32compat/w32fd.h | 68 +++----- contrib/win32/win32compat/wmain_common.c | 9 +- regress/pesterTests/SSH.Tests.ps1 | 101 +++++++++-- session.c | 8 + 23 files changed, 404 insertions(+), 298 deletions(-) diff --git a/contrib/win32/openssh/config.h.vs b/contrib/win32/openssh/config.h.vs index 7d5ea2ecf6ce..bb5030ec1600 100644 --- a/contrib/win32/openssh/config.h.vs +++ b/contrib/win32/openssh/config.h.vs @@ -297,7 +297,7 @@ /* Define to 1 if you have the declaration of `O_NONBLOCK', and to 0 if you don't. */ -#define HAVE_DECL_O_NONBLOCK 0 +#define HAVE_DECL_O_NONBLOCK 1 /* Define to 1 if you have the declaration of `passwdexpired', and to 0 if you don't. */ diff --git a/contrib/win32/openssh/keygen.vcxproj b/contrib/win32/openssh/keygen.vcxproj index 690ec33daee1..ac196225a034 100644 --- a/contrib/win32/openssh/keygen.vcxproj +++ b/contrib/win32/openssh/keygen.vcxproj @@ -1,4 +1,4 @@ - + @@ -186,6 +186,7 @@ + @@ -193,4 +194,4 @@ - \ No newline at end of file + diff --git a/contrib/win32/openssh/keygen.vcxproj.filters b/contrib/win32/openssh/keygen.vcxproj.filters index dd9dd237ddbb..8c39fa3e6b28 100644 --- a/contrib/win32/openssh/keygen.vcxproj.filters +++ b/contrib/win32/openssh/keygen.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -21,10 +21,13 @@ Source Files + + Source Files + Resource Files - \ No newline at end of file + diff --git a/contrib/win32/openssh/scp.vcxproj b/contrib/win32/openssh/scp.vcxproj index 7a922545283f..1a473315d6b6 100644 --- a/contrib/win32/openssh/scp.vcxproj +++ b/contrib/win32/openssh/scp.vcxproj @@ -1,4 +1,4 @@ - + @@ -22,6 +22,7 @@ + @@ -191,4 +192,4 @@ - \ No newline at end of file + diff --git a/contrib/win32/openssh/scp.vcxproj.filters b/contrib/win32/openssh/scp.vcxproj.filters index fccf50e03fdd..e37843803950 100644 --- a/contrib/win32/openssh/scp.vcxproj.filters +++ b/contrib/win32/openssh/scp.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -21,10 +21,13 @@ Source Files + + Source Files + Resource Files - \ No newline at end of file + diff --git a/contrib/win32/openssh/sftp.vcxproj b/contrib/win32/openssh/sftp.vcxproj index b558033582f7..c7ffb6dacfd0 100644 --- a/contrib/win32/openssh/sftp.vcxproj +++ b/contrib/win32/openssh/sftp.vcxproj @@ -1,4 +1,4 @@ - + @@ -26,6 +26,7 @@ + @@ -197,4 +198,4 @@ - \ No newline at end of file + diff --git a/contrib/win32/openssh/sftp.vcxproj.filters b/contrib/win32/openssh/sftp.vcxproj.filters index 04bc652032f0..60da32cc3ae8 100644 --- a/contrib/win32/openssh/sftp.vcxproj.filters +++ b/contrib/win32/openssh/sftp.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -33,10 +33,13 @@ Source Files + + Source Files + Resource Files - \ No newline at end of file + diff --git a/contrib/win32/openssh/ssh.vcxproj b/contrib/win32/openssh/ssh.vcxproj index d727934419f2..83c43367afcd 100644 --- a/contrib/win32/openssh/ssh.vcxproj +++ b/contrib/win32/openssh/ssh.vcxproj @@ -1,4 +1,4 @@ - + @@ -298,8 +298,9 @@ - + + @@ -307,4 +308,4 @@ - \ No newline at end of file + diff --git a/contrib/win32/openssh/ssh.vcxproj.filters b/contrib/win32/openssh/ssh.vcxproj.filters index 8acdd0e2167a..fae9a0fd9f7f 100644 --- a/contrib/win32/openssh/ssh.vcxproj.filters +++ b/contrib/win32/openssh/ssh.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -311,10 +311,13 @@ Source Files + + Source Files + Resource Files - \ No newline at end of file + diff --git a/contrib/win32/openssh/win32iocompat.vcxproj b/contrib/win32/openssh/win32iocompat.vcxproj index c2b0024b2197..51c419992c7e 100644 --- a/contrib/win32/openssh/win32iocompat.vcxproj +++ b/contrib/win32/openssh/win32iocompat.vcxproj @@ -160,7 +160,6 @@ - diff --git a/contrib/win32/openssh/win32iocompat.vcxproj.filters b/contrib/win32/openssh/win32iocompat.vcxproj.filters index 2152b5faecd2..456abfedd729 100644 --- a/contrib/win32/openssh/win32iocompat.vcxproj.filters +++ b/contrib/win32/openssh/win32iocompat.vcxproj.filters @@ -19,7 +19,6 @@ - diff --git a/contrib/win32/win32compat/fileio.c b/contrib/win32/win32compat/fileio.c index 49da1ad57707..e699ac909c96 100644 --- a/contrib/win32/win32compat/fileio.c +++ b/contrib/win32/win32compat/fileio.c @@ -55,8 +55,9 @@ struct createFile_flags { DWORD dwFlagsAndAttributes; }; -int termio_initiate_read(struct w32_io* pio); -int termio_initiate_write(struct w32_io* pio, DWORD num_bytes); +int syncio_initiate_read(struct w32_io* pio); +int syncio_initiate_write(struct w32_io* pio, DWORD num_bytes); +int syncio_close(struct w32_io* pio); /* maps Win32 error to errno */ int @@ -440,11 +441,10 @@ fileio_read(struct w32_io* pio, void *dst, unsigned int max) } if (fileio_is_io_available(pio, TRUE) == FALSE) { - if (FILETYPE(pio) == FILE_TYPE_CHAR) { - if (-1 == termio_initiate_read(pio)) + if (pio->type == NONSOCK_SYNC_FD || FILETYPE(pio) == FILE_TYPE_CHAR) { + if (-1 == syncio_initiate_read(pio)) return -1; - } - else { + } else { if (-1 == fileio_ReadFileEx(pio, max)) { if ((FILETYPE(pio) == FILE_TYPE_PIPE) && (errno == ERROR_BROKEN_PIPE)) { @@ -571,46 +571,12 @@ fileio_write(struct w32_io* pio, const void *buf, unsigned int max) bytes_copied = min(max, pio->write_details.buf_size); memcpy(pio->write_details.buf, buf, bytes_copied); - if (FILETYPE(pio) == FILE_TYPE_CHAR) { - if (termio_initiate_write(pio, bytes_copied) == 0) { + if (pio->type == NONSOCK_SYNC_FD || FILETYPE(pio) == FILE_TYPE_CHAR) { + if (syncio_initiate_write(pio, bytes_copied) == 0) { pio->write_details.pending = TRUE; pio->write_details.remaining = bytes_copied; - } - else - return -1; - } else if ( FILETYPE(pio) == FILE_TYPE_PIPE && - GetNamedPipeInfo(WINHANDLE(pio), &pipe_flags, NULL, NULL, &pipe_instances) && - pipe_flags == PIPE_CLIENT_END && pipe_instances == 1) { - /* - * TODO - Figure out a better solution to this problem - * IO handle corresponding to this object (pio->handle) may be referring - * to something that isn't opened in overlapped mode. While all handles - * opened by this POSIX wrapper are opened in overlapped mode, other handles - * that are inherited (ex. via std i/o) are typically not. - * Ex. When we do this in Powershell - * $o = ssh.exe user@target hostname - * Powershell creates anonymous pipes (that do not support overlapped i.o) - * Calling asynchronous I/O APIs (WriteFileEx) for example will not work in - * those cases (the callback is never called and it typically manifests as a - * hang to end user - * - * This conditional logic is put in place to specifically handle Powershell - * redirection scenarios. Thinking behind these conditions - * - should be a pipe handle. console I/O is handled in termio.c, impacting file i/o - * scenarios not found yet. - * - pipe should be the client end. This is to skip pipes created internally in POSIX - * wrapper (by pipe() calls) - The write ends on these pipes are on server - * - pipe_instances == 1. This is to skip pipe handles created as part of Connect(AF_UNIX) - * sockets (that typically are created for unlimited instances). - * For such I/O we do a synchronous write. - */ - /* DebugBreak() */; - if (WriteFile(WINHANDLE(pio), pio->write_details.buf, bytes_copied, &bytes_copied, NULL) == FALSE) { - errno = errno_from_Win32LastError(); - debug3("write - WriteFile() ERROR:%d, io:%p", GetLastError(), pio); + } else return -1; - } - return bytes_copied; } else { if (WriteFileEx(WINHANDLE(pio), pio->write_details.buf, bytes_copied, &pio->write_overlapped, &WriteCompletionRoutine)) { @@ -753,8 +719,8 @@ fileio_on_select(struct w32_io* pio, BOOL rd) if (!pio->read_details.pending && !fileio_is_io_available(pio, rd)) /* initiate read, record any error so read() will pick up */ - if (FILETYPE(pio) == FILE_TYPE_CHAR) { - if (termio_initiate_read(pio) != 0) { + if (pio->type == NONSOCK_SYNC_FD || FILETYPE(pio) == FILE_TYPE_CHAR) { + if (syncio_initiate_read(pio) != 0) { pio->read_details.error = errno; errno = 0; return; @@ -773,6 +739,9 @@ fileio_close(struct w32_io* pio) { debug4("fileclose - pio:%p", pio); + if (pio->type == NONSOCK_SYNC_FD || FILETYPE(pio) == FILE_TYPE_CHAR) + return syncio_close(pio); + /* handle can be null on AF_UNIX sockets that are not yet connected */ if (WINHANDLE(pio) == 0 || WINHANDLE(pio) == INVALID_HANDLE_VALUE) { free(pio); @@ -782,15 +751,13 @@ fileio_close(struct w32_io* pio) CancelIo(WINHANDLE(pio)); /* let queued APCs (if any) drain */ SleepEx(0, TRUE); - if (pio->type != STD_IO_FD) { /* STD handles are never explicitly closed */ - CloseHandle(WINHANDLE(pio)); - + CloseHandle(WINHANDLE(pio)); + /* free up non stdio */ + if (!IS_STDIO(pio)) { if (pio->read_details.buf) free(pio->read_details.buf); - if (pio->write_details.buf) free(pio->write_details.buf); - free(pio); } return 0; diff --git a/contrib/win32/win32compat/inc/fcntl.h b/contrib/win32/win32compat/inc/fcntl.h index 3f651493347e..fba482ee4c23 100644 --- a/contrib/win32/win32compat/inc/fcntl.h +++ b/contrib/win32/win32compat/inc/fcntl.h @@ -22,7 +22,6 @@ int w32_open(const char *pathname, int flags, ...); void* w32_fd_to_handle(int fd); int w32_allocate_fd_for_handle(void* h, int is_sock); -#define O_ACCMODE 0x0003 #define O_RDONLY _O_RDONLY #define O_WRONLY _O_WRONLY #define O_RDWR _O_RDWR @@ -37,4 +36,26 @@ int w32_allocate_fd_for_handle(void* h, int is_sock); #define O_NOINHERIT _O_NOINHERIT #define O_SEQUENTIAL _O_SEQUENTIAL #define O_RANDOM _O_RANDOM -#define O_U16TEXT _O_U16TEXT \ No newline at end of file +#define O_U16TEXT _O_U16TEXT + +/* +* open() POSIX specific modes and flags. +* Caution while making changes +* - cross check conflict with common macros in Windows headers +* - Ex. #define O_APPEND 0x8 +*/ +#define O_ACCMODE 0x0003 +#define O_NONBLOCK 0x0004 /*io operations wont block*/ +# define S_IXUSR 0000100 /* execute/search permission, */ +# define S_IXGRP 0000010 /* execute/search permission, */ +# define S_IXOTH 0000001 /* execute/search permission, */ +# define _S_IWUSR 0000200 /* write permission, */ +# define S_IWUSR _S_IWUSR /* write permission, owner */ +# define S_IWGRP 0000020 /* write permission, group */ +# define S_IWOTH 0000002 /* write permission, other */ +# define S_IRUSR 0000400 /* read permission, owner */ +# define S_IRGRP 0000040 /* read permission, group */ +# define S_IROTH 0000004 /* read permission, other */ +# define S_IRWXU 0000700 /* read, write, execute */ +# define S_IRWXG 0000070 /* read, write, execute */ +# define S_IRWXO 0000007 /* read, write, execute */ \ No newline at end of file diff --git a/contrib/win32/win32compat/misc.c b/contrib/win32/win32compat/misc.c index 93b1af3946a2..99bbaf3bdf09 100644 --- a/contrib/win32/win32compat/misc.c +++ b/contrib/win32/win32compat/misc.c @@ -404,123 +404,6 @@ w32_ioctl(int d, int request, ...) } } -/* - * spawn a child process - * - specified by cmd with agruments argv - * - with std handles set to in, out, err - * - flags are passed to CreateProcess call - * - * cmd will be internally decoarated with a set of '"' - * to account for any spaces within the commandline - * this decoration is done only when additional arguments are passed in argv - */ -int -spawn_child(char* cmd, char** argv, int in, int out, int err, DWORD flags) -{ - PROCESS_INFORMATION pi; - STARTUPINFOW si; - BOOL b; - char *cmdline, *t, **t1; - DWORD cmdline_len = 0; - wchar_t * cmdline_utf16; - int add_module_path = 0, ret = -1; - - /* should module path be added */ - do { - if (!cmd) - break; - t = cmd; - if (*t == '\"') - t++; - if (t[0] == '\0' || t[0] == '\\' || t[0] == '.' || t[1] == ':') - break; - add_module_path = 1; - } while (0); - - /* compute total cmdline len*/ - if (add_module_path) - cmdline_len += strlen(w32_programdir()) + 1 + strlen(cmd) + 1 + 2; - else - cmdline_len += strlen(cmd) + 1 + 2; - - if (argv) { - t1 = argv; - while (*t1) - cmdline_len += strlen(*t1++) + 1 + 2; - } - - if ((cmdline = malloc(cmdline_len)) == NULL) { - errno = ENOMEM; - goto cleanup; - } - - /* add current module path to start if needed */ - t = cmdline; - if (argv && argv[0]) - *t++ = '\"'; - if (add_module_path) { - memcpy(t, w32_programdir(), strlen(w32_programdir())); - t += strlen(w32_programdir()); - *t++ = '\\'; - } - - memcpy(t, cmd, strlen(cmd)); - t += strlen(cmd); - - if (argv && argv[0]) - *t++ = '\"'; - - if (argv) { - t1 = argv; - while (*t1) { - *t++ = ' '; - *t++ = '\"'; - memcpy(t, *t1, strlen(*t1)); - t += strlen(*t1); - *t++ = '\"'; - t1++; - } - } - - *t = '\0'; - - if ((cmdline_utf16 = utf8_to_utf16(cmdline)) == NULL) { - errno = ENOMEM; - goto cleanup; - } - - memset(&si, 0, sizeof(STARTUPINFOW)); - si.cb = sizeof(STARTUPINFOW); - si.hStdInput = w32_fd_to_handle(in); - si.hStdOutput = w32_fd_to_handle(out); - si.hStdError = w32_fd_to_handle(err); - si.dwFlags = STARTF_USESTDHANDLES; - - debug3("spawning %ls", cmdline_utf16); - b = CreateProcessW(NULL, cmdline_utf16, NULL, NULL, TRUE, flags, NULL, NULL, &si, &pi); - - if (b) { - if (register_child(pi.hProcess, pi.dwProcessId) == -1) { - TerminateProcess(pi.hProcess, 0); - CloseHandle(pi.hProcess); - goto cleanup; - } - CloseHandle(pi.hThread); - } else { - errno = GetLastError(); - goto cleanup; - } - - ret = pi.dwProcessId; -cleanup: - if (cmdline) - free(cmdline); - if (cmdline_utf16) - free(cmdline_utf16); - - return ret; -} - void strmode(mode_t mode, char *p) { diff --git a/contrib/win32/win32compat/misc_internal.h b/contrib/win32/win32compat/misc_internal.h index 6150de03a811..fa9a0d7b5d09 100644 --- a/contrib/win32/win32compat/misc_internal.h +++ b/contrib/win32/win32compat/misc_internal.h @@ -1,5 +1,8 @@ #pragma once #define PATH_MAX MAX_PATH +#define SSH_ASYNC_STDIN "SSH_ASYNC_STDIN" +#define SSH_ASYNC_STDOUT "SSH_ASYNC_STDOUT" +#define SSH_ASYNC_STDERR "SSH_ASYNC_STDERR" /* removes first '/' for Windows paths that are unix styled. Ex: /c:/ab.cd */ char * sanitized_path(const char *); diff --git a/contrib/win32/win32compat/signal.c b/contrib/win32/win32compat/signal.c index 035f79ce7ded..d0499031ba1f 100644 --- a/contrib/win32/win32compat/signal.c +++ b/contrib/win32/win32compat/signal.c @@ -78,7 +78,7 @@ sigtstp_APCProc(_In_ ULONG_PTR dwParam) BOOL WINAPI native_sig_handler(DWORD dwCtrlType) { - debug3("Native Ctrl+C handler, CtrlType %d", dwCtrlType); + debug4("Native Ctrl+C handler, CtrlType %d", dwCtrlType); switch (dwCtrlType) { case CTRL_C_EVENT: QueueUserAPC(sigint_APCProc, main_thread, (ULONG_PTR)NULL); @@ -154,7 +154,7 @@ w32_sigprocmask(int how, const sigset_t *set, sigset_t *oldset) int w32_raise(int sig) { - debug3("raise sig:%d", sig); + debug4("raise sig:%d", sig); if (sig == W32_SIGSEGV) return raise(SIGSEGV); /* raise native exception handler*/ @@ -229,7 +229,7 @@ sw_process_pending_signals() DebugBreak(); if (sig_int) { - debug3("process_queued_signals: WARNING - A signal has interrupted and was processed"); + debug4("process_queued_signals: WARNING - A signal has interrupted and was processed"); errno = EINTR; return -1; } diff --git a/contrib/win32/win32compat/socketio.c b/contrib/win32/win32compat/socketio.c index 804520dd0e0c..5e9d6621f4e0 100644 --- a/contrib/win32/win32compat/socketio.c +++ b/contrib/win32/win32compat/socketio.c @@ -110,7 +110,7 @@ socketio_acceptEx(struct w32_io* pio) context = (struct acceptEx_context *)pio->internal.context; ResetEvent(pio->read_overlapped.hEvent); - if (getsockname(pio->sock, &addr, &addrlen) == SOCKET_ERROR) { + if (getsockname(pio->sock, (struct sockaddr*)&addr, &addrlen) == SOCKET_ERROR) { errno = errno_from_WSALastError(); debug("acceptEx - getsockname() ERROR:%d, io:%p", errno, pio); return -1; diff --git a/contrib/win32/win32compat/termio.c b/contrib/win32/win32compat/termio.c index 33ed14ef0f62..945e227ba7ed 100644 --- a/contrib/win32/win32compat/termio.c +++ b/contrib/win32/win32compat/termio.c @@ -9,6 +9,10 @@ * Author: Balu * Misc fixes and code cleanup * + * Author: Manoj Ampalam + * Extended support to other Windows IO that does not support + * overlapped IO. Ex. pipe handles returned by CreatePipe() + * * Copyright (c) 2017 Microsoft Corp. * All rights reserved * @@ -71,18 +75,26 @@ ReadAPCProc(_In_ ULONG_PTR dwParam) /* Read worker thread */ static DWORD WINAPI -ReadConsoleThread(_In_ LPVOID lpParameter) +ReadThread(_In_ LPVOID lpParameter) { int nBytesReturned = 0; struct w32_io* pio = (struct w32_io*)lpParameter; debug5("TermRead thread, io:%p", pio); memset(&read_status, 0, sizeof(read_status)); - while (nBytesReturned == 0) { - nBytesReturned = ReadConsoleForTermEmul(WINHANDLE(pio), - pio->read_details.buf, pio->read_details.buf_size); + if (FILETYPE(pio) == FILE_TYPE_CHAR) { + while (nBytesReturned == 0) { + nBytesReturned = ReadConsoleForTermEmul(WINHANDLE(pio), + pio->read_details.buf, pio->read_details.buf_size); + } + read_status.transferred = nBytesReturned; + } else { + if (!ReadFile(WINHANDLE(pio), pio->read_details.buf, + pio->read_details.buf_size, &read_status.transferred, NULL)) { + read_status.error = GetLastError(); + debug("ReadThread - ReadFile failed %d, io:%p", GetLastError(), pio); + } } - read_status.transferred = nBytesReturned; if (0 == QueueUserAPC(ReadAPCProc, main_thread, (ULONG_PTR)pio)) { debug3("TermRead thread - ERROR QueueUserAPC failed %d, io:%p", GetLastError(), pio); pio->read_details.pending = FALSE; @@ -95,11 +107,11 @@ ReadConsoleThread(_In_ LPVOID lpParameter) /* Initiates read on tty */ int -termio_initiate_read(struct w32_io* pio) +syncio_initiate_read(struct w32_io* pio) { HANDLE read_thread; - debug5("TermRead initiate io:%p", pio); + debug5("syncio_initiate_read io:%p", pio); if (pio->read_details.buf_size == 0) { pio->read_details.buf = malloc(TERM_IO_BUF_SIZE); if (pio->read_details.buf == NULL) { @@ -109,7 +121,7 @@ termio_initiate_read(struct w32_io* pio) pio->read_details.buf_size = TERM_IO_BUF_SIZE; } - read_thread = CreateThread(NULL, 0, ReadConsoleThread, pio, 0, NULL); + read_thread = CreateThread(NULL, 0, ReadThread, pio, 0, NULL); if (read_thread == NULL) { errno = errno_from_Win32Error(GetLastError()); debug3("TermRead initiate - ERROR CreateThread %d, io:%p", GetLastError(), pio); @@ -148,19 +160,26 @@ WriteThread(_In_ LPVOID lpParameter) size_t resplen = 0; debug5("TermWrite thread, io:%p", pio); - pio->write_details.buf[write_status.to_transfer] = '\0'; - - if (0 == in_raw_mode) { - wchar_t* t = utf8_to_utf16(pio->write_details.buf); - WriteConsoleW(WINHANDLE(pio), t, wcslen(t), 0, 0); - free(t); + if (FILETYPE(pio) == FILE_TYPE_CHAR) { + pio->write_details.buf[write_status.to_transfer] = '\0'; + if (0 == in_raw_mode) { + wchar_t* t = utf8_to_utf16(pio->write_details.buf); + WriteConsoleW(WINHANDLE(pio), t, wcslen(t), 0, 0); + free(t); + } else { + processBuffer(WINHANDLE(pio), pio->write_details.buf, write_status.to_transfer, &respbuf, &resplen); + /* TODO - respbuf is not null in some cases, this needs to be returned back via read stream */ + } + write_status.transferred = write_status.to_transfer; } else { - processBuffer(WINHANDLE(pio), pio->write_details.buf, write_status.to_transfer, &respbuf, &resplen); - /* TODO - respbuf is not null in some cases, this needs to be returned back via read stream */ + if (!WriteFile(WINHANDLE(pio), pio->write_details.buf, write_status.to_transfer, + &write_status.transferred, NULL)) { + write_status.error = GetLastError(); + debug("WriteThread - ReadFile WriteFile %d, io:%p", GetLastError(), pio); + } } - write_status.transferred = write_status.to_transfer; - + if (0 == QueueUserAPC(WriteAPCProc, main_thread, (ULONG_PTR)pio)) { debug3("TermWrite thread - ERROR QueueUserAPC failed %d, io:%p", GetLastError(), pio); pio->write_details.pending = FALSE; @@ -173,7 +192,7 @@ WriteThread(_In_ LPVOID lpParameter) /* Initiates write on tty */ int -termio_initiate_write(struct w32_io* pio, DWORD num_bytes) +syncio_initiate_write(struct w32_io* pio, DWORD num_bytes) { HANDLE write_thread; debug5("TermWrite initiate io:%p", pio); @@ -193,21 +212,27 @@ termio_initiate_write(struct w32_io* pio, DWORD num_bytes) /* tty close */ int -termio_close(struct w32_io* pio) +syncio_close(struct w32_io* pio) { - debug4("termio_close - pio:%p", pio); + debug4("syncio_close - pio:%p", pio); HANDLE h; CancelIoEx(WINHANDLE(pio), NULL); - /* If io is pending, let write worker threads exit. The read thread is blocked so terminate it.*/ - if (pio->read_details.pending) - TerminateThread(pio->read_overlapped.hEvent, 0); + + /* If io is pending, let worker threads exit. */ + if (pio->read_details.pending) { + /* For console - the read thread is blocked so terminate it. */ + if (FILETYPE(pio) == FILE_TYPE_CHAR) + TerminateThread(pio->read_overlapped.hEvent, 0); + else + WaitForSingleObject(pio->read_overlapped.hEvent, INFINITE); + } if (pio->write_details.pending) WaitForSingleObject(pio->write_overlapped.hEvent, INFINITE); /* drain queued APCs */ SleepEx(0, TRUE); - if (pio->type != STD_IO_FD) { - /* STD handles are never explicitly closed */ - CloseHandle(WINHANDLE(pio)); + CloseHandle(WINHANDLE(pio)); + /* free up if non stdio */ + if (!IS_STDIO(pio)) { if (pio->read_details.buf) free(pio->read_details.buf); if (pio->write_details.buf) diff --git a/contrib/win32/win32compat/w32fd.c b/contrib/win32/win32compat/w32fd.c index 4e48f9644d9c..3921fd44e74b 100644 --- a/contrib/win32/win32compat/w32fd.c +++ b/contrib/win32/win32compat/w32fd.c @@ -36,6 +36,7 @@ #include "inc\unistd.h" #include "inc\fcntl.h" #include "inc\sys\un.h" +#include "inc\utf.h" #include "w32fd.h" #include "signal_internal.h" @@ -74,15 +75,24 @@ fd_table_initialize() memset(&fd_table, 0, sizeof(fd_table)); memset(&w32_io_stdin, 0, sizeof(w32_io_stdin)); w32_io_stdin.std_handle = STD_INPUT_HANDLE; - w32_io_stdin.type = STD_IO_FD; + w32_io_stdin.type = NONSOCK_SYNC_FD; + if (getenv(SSH_ASYNC_STDIN) && strcmp(getenv(SSH_ASYNC_STDIN), "1") == 0) + w32_io_stdin.type = NONSOCK_FD; + _putenv_s(SSH_ASYNC_STDIN, ""); fd_table_set(&w32_io_stdin, STDIN_FILENO); memset(&w32_io_stdout, 0, sizeof(w32_io_stdout)); w32_io_stdout.std_handle = STD_OUTPUT_HANDLE; - w32_io_stdout.type = STD_IO_FD; + w32_io_stdout.type = NONSOCK_SYNC_FD; + if (getenv(SSH_ASYNC_STDOUT) && strcmp(getenv(SSH_ASYNC_STDOUT), "1") == 0) + w32_io_stdout.type = NONSOCK_FD; + _putenv_s(SSH_ASYNC_STDOUT, ""); fd_table_set(&w32_io_stdout, STDOUT_FILENO); memset(&w32_io_stderr, 0, sizeof(w32_io_stderr)); w32_io_stderr.std_handle = STD_ERROR_HANDLE; - w32_io_stderr.type = STD_IO_FD; + w32_io_stderr.type = NONSOCK_SYNC_FD; + if (getenv(SSH_ASYNC_STDERR) && strcmp(getenv(SSH_ASYNC_STDERR), "1") == 0) + w32_io_stderr.type = NONSOCK_FD; + _putenv_s(SSH_ASYNC_STDERR, ""); fd_table_set(&w32_io_stderr, STDERR_FILENO); return 0; } @@ -128,7 +138,6 @@ fd_table_set(struct w32_io* pio, int index) static void fd_table_clear(int index) { - fd_table.w32_ios[index]->table_index = -1; fd_table.w32_ios[index] = NULL; FD_CLR(index, &(fd_table.occupied)); } @@ -483,6 +492,7 @@ int w32_close(int fd) { struct w32_io* pio; + int r; if ((fd < 0) || (fd > MAX_FDS - 1) || fd_table.w32_ios[fd] == NULL) { errno = EBADF; return -1; @@ -492,17 +502,14 @@ w32_close(int fd) debug3("close - io:%p, type:%d, fd:%d, table_index:%d", pio, pio->type, fd, pio->table_index); - fd_table_clear(pio->table_index); - + if (pio->type == SOCK_FD) - return socketio_close(pio); + r = socketio_close(pio); else - switch (FILETYPE(pio)) { - case FILE_TYPE_CHAR: - return termio_close(pio); - default: - return fileio_close(pio); - } + r = fileio_close(pio); + + fd_table_clear(fd); + return r; } static int @@ -798,7 +805,7 @@ w32_dup(int oldfd) memset(pio, 0, sizeof(struct w32_io)); pio->handle = target; - pio->type = NONSOCK_FD; + pio->type = fd_table.w32_ios[oldfd]->type; fd_table_set(pio, min_index); return min_index; } @@ -866,3 +873,131 @@ w32_fsync(int fd) CHECK_FD(fd); return FlushFileBuffers(w32_fd_to_handle(fd)); } + + +/* +* spawn a child process +* - specified by cmd with agruments argv +* - with std handles set to in, out, err +* - flags are passed to CreateProcess call +* +* cmd will be internally decoarated with a set of '"' +* to account for any spaces within the commandline +* this decoration is done only when additional arguments are passed in argv +*/ +int +spawn_child(char* cmd, char** argv, int in, int out, int err, DWORD flags) +{ + PROCESS_INFORMATION pi; + STARTUPINFOW si; + BOOL b; + char *cmdline, *t, **t1; + DWORD cmdline_len = 0; + wchar_t * cmdline_utf16; + int add_module_path = 0, ret = -1; + + /* should module path be added */ + do { + if (!cmd) + break; + t = cmd; + if (*t == '\"') + t++; + if (t[0] == '\0' || t[0] == '\\' || t[0] == '.' || t[1] == ':') + break; + add_module_path = 1; + } while (0); + + /* compute total cmdline len*/ + if (add_module_path) + cmdline_len += strlen(w32_programdir()) + 1 + strlen(cmd) + 1 + 2; + else + cmdline_len += strlen(cmd) + 1 + 2; + + if (argv) { + t1 = argv; + while (*t1) + cmdline_len += strlen(*t1++) + 1 + 2; + } + + if ((cmdline = malloc(cmdline_len)) == NULL) { + errno = ENOMEM; + goto cleanup; + } + + /* add current module path to start if needed */ + t = cmdline; + if (argv && argv[0]) + *t++ = '\"'; + if (add_module_path) { + memcpy(t, w32_programdir(), strlen(w32_programdir())); + t += strlen(w32_programdir()); + *t++ = '\\'; + } + + memcpy(t, cmd, strlen(cmd)); + t += strlen(cmd); + + if (argv && argv[0]) + *t++ = '\"'; + + if (argv) { + t1 = argv; + while (*t1) { + *t++ = ' '; + *t++ = '\"'; + memcpy(t, *t1, strlen(*t1)); + t += strlen(*t1); + *t++ = '\"'; + t1++; + } + } + + *t = '\0'; + + if ((cmdline_utf16 = utf8_to_utf16(cmdline)) == NULL) { + errno = ENOMEM; + goto cleanup; + } + + memset(&si, 0, sizeof(STARTUPINFOW)); + si.cb = sizeof(STARTUPINFOW); + si.hStdInput = w32_fd_to_handle(in); + si.hStdOutput = w32_fd_to_handle(out); + si.hStdError = w32_fd_to_handle(err); + si.dwFlags = STARTF_USESTDHANDLES; + + debug3("spawning %ls", cmdline_utf16); + if (fd_table.w32_ios[in]->type != NONSOCK_SYNC_FD) + _putenv_s(SSH_ASYNC_STDIN, "1"); + if (fd_table.w32_ios[out]->type != NONSOCK_SYNC_FD) + _putenv_s(SSH_ASYNC_STDOUT, "1"); + if (fd_table.w32_ios[err]->type != NONSOCK_SYNC_FD) + _putenv_s(SSH_ASYNC_STDERR, "1"); + b = CreateProcessW(NULL, cmdline_utf16, NULL, NULL, TRUE, flags, NULL, NULL, &si, &pi); + _putenv_s(SSH_ASYNC_STDIN, ""); + _putenv_s(SSH_ASYNC_STDOUT, ""); + _putenv_s(SSH_ASYNC_STDERR, ""); + + if (b) { + if (register_child(pi.hProcess, pi.dwProcessId) == -1) { + TerminateProcess(pi.hProcess, 0); + CloseHandle(pi.hProcess); + goto cleanup; + } + CloseHandle(pi.hThread); + } + else { + errno = GetLastError(); + goto cleanup; + } + + ret = pi.dwProcessId; +cleanup: + if (cmdline) + free(cmdline); + if (cmdline_utf16) + free(cmdline_utf16); + + return ret; +} diff --git a/contrib/win32/win32compat/w32fd.h b/contrib/win32/win32compat/w32fd.h index 0387a00f49e5..22f235927e9c 100644 --- a/contrib/win32/win32compat/w32fd.h +++ b/contrib/win32/win32compat/w32fd.h @@ -39,7 +39,22 @@ enum w32_io_type { UNKNOWN_FD = 0, SOCK_FD = 1, /*maps a socket fd*/ NONSOCK_FD = 2, /*maps a file fd, pipe fd or a tty fd*/ - STD_IO_FD = 5 /*maps a std fd - ex. STDIN_FILE*/ + /* + * maps a NONSOCK_FD that doesnt support async or overlapped io + * these are typically used for stdio on ssh client side + * executables (ssh, sftp and scp). + * Ex. ssh ... > output.txt + * In the above case, stdout passed to ssh.exe is a handle to + * output.txt that is opened in non-overlapped mode + * Ex. sample.exe | ssh ... + * In the above case, stdin passed to ssh.exe is a handle to + * a pipe opened in non-overlapped mode + * Ex. in Powershell + * $o = ssh ... + * In the above case, stdout passed to ssh.exe is a handle to + * a pipe opened in non-overlapped mode + */ + NONSOCK_SYNC_FD = 3 }; enum w32_io_sock_state { @@ -51,7 +66,7 @@ enum w32_io_sock_state { }; /* -* This structure encapsulates the state info needed to map a File Descriptor +* This structure encapsulates the I/O state info needed to map a File Descriptor * to Win32 Handle */ struct w32_io { @@ -94,7 +109,8 @@ struct w32_io { }internal; }; -#define WINHANDLE(pio) (((pio)->type == STD_IO_FD)? GetStdHandle((pio)->std_handle):(pio)->handle) +#define IS_STDIO(pio) ((pio)->table_index <= 2) +#define WINHANDLE(pio) (IS_STDIO(pio)? GetStdHandle((pio)->std_handle):(pio)->handle) #define FILETYPE(pio) (GetFileType(WINHANDLE(pio))) extern HANDLE main_thread; @@ -102,7 +118,7 @@ BOOL w32_io_is_blocking(struct w32_io*); BOOL w32_io_is_io_available(struct w32_io* pio, BOOL rd); int wait_for_any_event(HANDLE* events, int num_events, DWORD milli_seconds); -/*POSIX mimic'ing socket API*/ +/*POSIX mimic'ing socket API and socket helper API*/ int socketio_initialize(); int socketio_done(); BOOL socketio_is_io_available(struct w32_io* pio, BOOL rd); @@ -122,7 +138,7 @@ int socketio_send(struct w32_io* pio, const void *buf, size_t len, int flags); int socketio_shutdown(struct w32_io* pio, int how); int socketio_close(struct w32_io* pio); -/*POSIX mimic'ing file API*/ +/*POSIX mimic'ing file API and file helper API*/ BOOL fileio_is_io_available(struct w32_io* pio, BOOL rd); void fileio_on_select(struct w32_io* pio, BOOL rd); int fileio_close(struct w32_io* pio); @@ -136,45 +152,3 @@ int fileio_fstat(struct w32_io* pio, struct _stat64 *buf); int fileio_stat(const char *path, struct _stat64 *buf); long fileio_lseek(struct w32_io* pio, long offset, int origin); FILE* fileio_fdopen(struct w32_io* pio, const char *mode); - -/* terminal io specific versions */ -int termio_close(struct w32_io* pio); - -/* -* open() flags and modes -* all commented out macros are defined in fcntl.h -* they are listed here so as to cross check any conflicts with macros explicitly -* defined below. -*/ -/*open access modes. only one of these can be specified*/ -/* #define O_RDONLY 0x0 */ -/* #define O_WRONLY 0x1 */ -/* #define O_RDWR 0x2 */ -/* open file creation and file status flags can be bitwise-or'd*/ -/* #define O_APPEND 0x8 /*file is opened in append mode*/ -#ifndef O_NONBLOCK -#define O_NONBLOCK 0x0004 /*io operations wont block*/ -#endif -/* #define O_CREAT 0x100 /*If the file does not exist it will be created*/ -/* -* If the file exists and is a regular file, and the file is successfully -* opened O_RDWR or O_WRONLY, its length shall be truncated to 0, and the mode -* and owner shall be unchanged -*/ -/* #define O_TRUNC 0x200 */ -/* If O_CREAT and O_EXCL are set, open() shall fail if the file exists */ -/* #define O_EXCL 0x400 */ -/* #define O_BINARY 0x8000 //Gives raw data (while O_TEXT normalises line endings */ -/* open modes */ -#ifndef S_IRUSR -#define S_IRUSR 00400 /* user has read permission */ -#endif /* ! S_IRUSR */ -#ifndef S_IWUSR -#define S_IWUSR 00200 /* user has write permission */ -#endif -#ifndef S_IRGRP -#define S_IRGRP 00040 /* group has read permission */ -#endif -#ifndef S_IROTH -#define S_IROTH 00004 /* others have read permission */ -#endif \ No newline at end of file diff --git a/contrib/win32/win32compat/wmain_common.c b/contrib/win32/win32compat/wmain_common.c index 3d09ceeace29..fb6ca8f1e8d7 100644 --- a/contrib/win32/win32compat/wmain_common.c +++ b/contrib/win32/win32compat/wmain_common.c @@ -52,8 +52,9 @@ wmain(int argc, wchar_t **wargv) { if (getenv("SSH_AUTH_SOCK") == NULL) _putenv("SSH_AUTH_SOCK=ssh-agent"); - w32posix_initialize(); - r = main(argc, argv); - w32posix_done(); - return r; + w32posix_initialize(); + + r = main(argc, argv); + w32posix_done(); + return r; } diff --git a/regress/pesterTests/SSH.Tests.ps1 b/regress/pesterTests/SSH.Tests.ps1 index 58cb7a6ee6ed..74223bca57d7 100644 --- a/regress/pesterTests/SSH.Tests.ps1 +++ b/regress/pesterTests/SSH.Tests.ps1 @@ -1,14 +1,30 @@ -#covered -i -q -v -l -c -C +#todo: -i -q -v -l -c -C #todo: -S -F -V -e -Describe "Tests for ssh command" -Tags "Scenario" { +$tB = 1 +$tI = 0 + +Describe "ssh client tests" -Tags "CI" { BeforeAll { - $fileName = "test.txt" - $filePath = Join-Path ${TestDrive} $fileName + if($OpenSSHTestInfo -eq $null) + { + Throw "`$OpenSSHTestInfo is null. Please run Setup-OpenSSHTestEnvironment to setup test environment." + } + + if(-not (Test-Path $OpenSSHTestInfo["TestDataPath"])) + { + $null = New-Item $OpenSSHTestInfo["TestDataPath"] -ItemType directory -Force -ErrorAction SilentlyContinue + } - [Machine] $client = [Machine]::new([MachineRole]::Client) - [Machine] $server = [Machine]::new([MachineRole]::Server) - $client.SetupClient($server) - $server.SetupServer($client) + $server = $OpenSSHTestInfo["Target"] + $port = $OpenSSHTestInfo["Port"] + $ssouser = $OpenSSHTestInfo["SSOUser"] + $sshCmdDefault = "ssh -p $port $($ssouser)@$($server)" + + $testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] "ssh" + if(-not (Test-Path $testDir)) + { + $null = New-Item $testDir -ItemType directory -Force -ErrorAction SilentlyContinue + } $testData = @( @{ @@ -55,12 +71,71 @@ Describe "Tests for ssh command" -Tags "Scenario" { } - AfterAll { - $client.CleanupClient() - $server.CleanupServer() + BeforeEach { + $tI++; + $tFile=Join-Path $testDir "$tB.$tI.txt" + } + + Context "$tB - Basic Scenarios" { + + BeforeAll {$tI=1} + AfterAll{$tB++} + + <# these 2 tests dont work on AppVeyor that sniffs stderr channel + It "$tB.$tI - test version" { + iex "ssh -V 2> $tFile" + $tFile | Should Contain "OpenSSH_" + } + + It "$tB.$tI - test help" { + iex "ssh -? 2> $tFile" + $tFile | Should Contain "usage: ssh" + } + #> + + It "$tB.$tI - remote echo command" { + iex "$sshDefaultCmd echo 1234" | Should Be "1234" + } + } + + Context "$tB - Redirection Scenarios" { + + BeforeAll {$tI=1} + AfterAll{$tB++} + + It "$tB.$tI - stdout to file" { + iex "$sshDefaultCmd powershell get-process > $tFile" + $tFile | Should Contain "ProcessName" + } + + It "$tB.$tI - stdout to PS object" { + $o = iex "$sshDefaultCmd echo 1234" + $o | Should Be "1234" + } + + <#It "$tB.$tI - stdin from PS object" { + #if input redirection doesn't work, this would hang + 0 | ssh -p $port $ssouser@$server pause + $true | Should Be $true + }#> } - Context "Key is not secured in ssh-agent on server" { + Context "$tB - cmdline parameters" { + + BeforeAll {$tI=1} + AfterAll{$tB++} + + It "$tB.$tI - verbose to file" { + $logFile = Join-Path $testDir "$tB.$tI.log.txt" + $o = ssh -p $port -v -E $logFile $ssouser@$server echo 1234 + $o | Should Be "1234" + #TODO - checks below are very inefficient (time taking). + $logFile | Should Contain "OpenSSH_" + $logFile | Should Contain "Exit Status 0" + } + + } + <#Context "Key is not secured in ssh-agent on server" { BeforeAll { $identifyFile = $client.clientPrivateKeyPaths[0] Remove-Item -Path $filePath -Force -ea silentlycontinue @@ -156,5 +231,5 @@ Describe "Tests for ssh command" -Tags "Scenario" { #validate file content. Get-Content $filePath | Should be $server.MachineName } - } + }#> } diff --git a/session.c b/session.c index 523acf4fada2..a689b27ad113 100644 --- a/session.c +++ b/session.c @@ -559,6 +559,10 @@ int do_exec_windows(Session *s, const char *command, int pty) { debug("Executing command: %s", exec_command); UTF8_TO_UTF16_FATAL(exec_command_w, exec_command); + _putenv_s("SSH_ASYNC_STDIN", "1"); + _putenv_s("SSH_ASYNC_STDOUT", "1"); + _putenv_s("SSH_ASYNC_STDERR", "1"); + /* in debug mode launch using sshd.exe user context */ if (debug_flag) b = CreateProcessW(NULL, exec_command_w, NULL, NULL, TRUE, @@ -569,6 +573,10 @@ int do_exec_windows(Session *s, const char *command, int pty) { DETACHED_PROCESS , NULL, pw_dir_w, &si, &pi); + _putenv_s("SSH_ASYNC_STDIN", ""); + _putenv_s("SSH_ASYNC_STDOUT", ""); + _putenv_s("SSH_ASYNC_STDERR", ""); + if (!b) fatal("ERROR. Cannot create process (%u).\n", GetLastError()); else if (pty) { /*attach to shell console */