diff --git a/channels/sshagent/CMakeLists.txt b/channels/sshagent/CMakeLists.txt new file mode 100644 index 000000000..bc6cb2fc5 --- /dev/null +++ b/channels/sshagent/CMakeLists.txt @@ -0,0 +1,26 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +define_channel("sshagent") + +if(WITH_CLIENT_CHANNELS) + add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() + +if(WITH_SERVER_CHANNELS) + add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME}) +endif() diff --git a/channels/sshagent/ChannelOptions.cmake b/channels/sshagent/ChannelOptions.cmake new file mode 100644 index 000000000..083d8d54c --- /dev/null +++ b/channels/sshagent/ChannelOptions.cmake @@ -0,0 +1,13 @@ + +set(OPTION_DEFAULT OFF) +set(OPTION_CLIENT_DEFAULT OFF) +set(OPTION_SERVER_DEFAULT OFF) + +define_channel_options(NAME "sshagent" TYPE "dynamic" + DESCRIPTION "SSH Agent Forwarding Extension" + SPECIFICATIONS "" + DEFAULT ${OPTION_DEFAULT}) + +define_channel_client_options(${OPTION_CLIENT_DEFAULT}) +define_channel_server_options(${OPTION_SERVER_DEFAULT}) + diff --git a/channels/sshagent/client/CMakeLists.txt b/channels/sshagent/client/CMakeLists.txt new file mode 100644 index 000000000..bf8b1b8f8 --- /dev/null +++ b/channels/sshagent/client/CMakeLists.txt @@ -0,0 +1,33 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP cmake build script +# +# Copyright 2012 Marc-Andre Moreau +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +define_channel_client("sshagent") + +set(${MODULE_PREFIX}_SRCS + sshagent_main.c + sshagent_main.h) + +include_directories(..) + +add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry") + +if (WITH_DEBUG_SYMBOLS AND MSVC AND NOT BUILTIN_CHANNELS AND BUILD_SHARED_LIBS) + install(FILES ${CMAKE_BINARY_DIR}/${MODULE_NAME}.pdb DESTINATION ${FREERDP_ADDIN_PATH} COMPONENT symbols) +endif() + +target_link_libraries(${MODULE_NAME} winpr) +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client") diff --git a/channels/sshagent/client/sshagent_main.c b/channels/sshagent/client/sshagent_main.c new file mode 100644 index 000000000..61e6aaa1f --- /dev/null +++ b/channels/sshagent/client/sshagent_main.c @@ -0,0 +1,398 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Echo Virtual Channel Extension + * + * Copyright 2013 Christian Hofstaedtler + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * Copyright 2017 Ben Cohen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * sshagent_main.c: DVC plugin to forward queries from RDP to the ssh-agent + * + * This relays data to and from an ssh-agent program equivalent running on the + * RDP server to an ssh-agent running locally. Unlike the normal ssh-agent, + * which sends data over an SSH channel, the data is send over an RDP dynamic + * virtual channel. + * + * protocol specification: + * Forward data verbatim over RDP dynamic virtual channel named "sshagent" + * between a ssh client on the xrdp server and the real ssh-agent where + * the RDP client is running. Each connection by a separate client to + * xrdp-ssh-agent gets a separate DVC invocation. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "sshagent_main.h" +#include + +#define TAG CHANNELS_TAG("sshagent.client") + +typedef struct _SSHAGENT_LISTENER_CALLBACK SSHAGENT_LISTENER_CALLBACK; +struct _SSHAGENT_LISTENER_CALLBACK +{ + IWTSListenerCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + + const char *agent_uds_path; +}; + +typedef struct _SSHAGENT_CHANNEL_CALLBACK SSHAGENT_CHANNEL_CALLBACK; +struct _SSHAGENT_CHANNEL_CALLBACK +{ + IWTSVirtualChannelCallback iface; + + IWTSPlugin* plugin; + IWTSVirtualChannelManager* channel_mgr; + IWTSVirtualChannel* channel; + + int agent_fd; + HANDLE thread; + CRITICAL_SECTION lock; +}; + +typedef struct _SSHAGENT_PLUGIN SSHAGENT_PLUGIN; +struct _SSHAGENT_PLUGIN +{ + IWTSPlugin iface; + + SSHAGENT_LISTENER_CALLBACK* listener_callback; +}; + + +/** + * Function to open the connection to the sshagent + * + * @return The fd on success, otherwise -1 + */ +static int connect_to_sshagent(const char *udspath) +{ + int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (agent_fd == -1) + { + WLog_ERR(TAG, "Can't open Unix domain socket!"); + return -1; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1); + int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr)); + if (rc != 0) + { + WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", + udspath); + return -1; + } + + return agent_fd; +} + + +/** + * Entry point for thread to read from the ssh-agent socket and forward + * the data to RDP + * + * @return NULL + */ +static void *sshagent_read_thread(void *data) +{ + SSHAGENT_CHANNEL_CALLBACK *callback = (SSHAGENT_CHANNEL_CALLBACK *)data; + BYTE buffer[4096]; + int going = 1; + UINT status = CHANNEL_RC_OK; + + while (going) + { + int bytes_read = read(callback->agent_fd, + buffer, + sizeof(buffer)); + + if (bytes_read == 0) + { + /* Socket closed cleanly at other end */ + going = 0; + } + else if (bytes_read < 0) + { + if (errno != EAGAIN + && errno != EWOULDBLOCK + && errno != EINTR) + { + WLog_ERR(TAG, + "Error reading from sshagent, errno=%d", + errno); + status = ERROR_READ_FAULT; + going = 0; + } + } + else + { + /* Something read: forward to virtual channel */ + status = callback->channel->Write(callback->channel, + bytes_read, + buffer, + NULL); + if (status != CHANNEL_RC_OK) + { + going = 0; + } + } + } + + close(callback->agent_fd); + + //if (status != CHANNEL_RC_OK) + // setChannelError(rdpei->rdpcontext, status, + // "sshagent_read_thread reported an error"); + + ExitThread(0); + return NULL; +} + +/** + * Callback for data received from the RDP server; forward this to ssh-agent + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream *data) +{ + SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*) pChannelCallback; + BYTE* pBuffer = Stream_Pointer(data); + UINT32 cbSize = Stream_GetRemainingLength(data); + BYTE *pos = pBuffer; + + /* Forward what we have received to the ssh agent */ + UINT32 bytes_to_write = cbSize; + errno = 0; + while (bytes_to_write > 0) + { + int bytes_written = write(callback->agent_fd, pos, + bytes_to_write); + if (bytes_written < 0) + { + if (errno != EAGAIN + && errno != EWOULDBLOCK + && errno != EINTR) + { + WLog_ERR(TAG, + "Error writing to sshagent, errno=%d", + errno); + return ERROR_WRITE_FAULT; + } + } + else + { + bytes_to_write -= bytes_written; + pos += bytes_written; + } + } + + /* Consume stream */ + Stream_Seek(data, cbSize); + + return CHANNEL_RC_OK; +} + +/** + * Callback for when the virtual channel is closed + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback) +{ + SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*) pChannelCallback; + + /* Call shutdown() to wake up the read() in sshagent_read_thread(). */ + shutdown(callback->agent_fd, SHUT_RDWR); + + EnterCriticalSection(&callback->lock); + if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED) + { + UINT error = GetLastError(); + WLog_ERR(TAG, "WaitForSingleObject failed with error %"PRIu32"!", error); + return error; + } + + CloseHandle(callback->thread); + DeleteCriticalSection(&callback->lock); + + free(callback); + + return CHANNEL_RC_OK; +} + + +/** + * Callback for when a new virtual channel is opened + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_on_new_channel_connection(IWTSListenerCallback* pListenerCallback, + IWTSVirtualChannel* pChannel, BYTE* Data, BOOL* pbAccept, + IWTSVirtualChannelCallback** ppCallback) +{ + SSHAGENT_CHANNEL_CALLBACK* callback; + SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*) pListenerCallback; + + callback = (SSHAGENT_CHANNEL_CALLBACK*) calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK)); + + if (!callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + /* Now open a connection to the local ssh-agent. Do this for each + * connection to the plugin in case we mess up the agent session. */ + callback->agent_fd + = connect_to_sshagent(listener_callback->agent_uds_path); + if (callback->agent_fd == -1) + { + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + InitializeCriticalSection(&callback->lock); + + callback->iface.OnDataReceived = sshagent_on_data_received; + callback->iface.OnClose = sshagent_on_close; + callback->plugin = listener_callback->plugin; + callback->channel_mgr = listener_callback->channel_mgr; + callback->channel = pChannel; + + if (!(callback->thread + = CreateThread(NULL, + 0, + (LPTHREAD_START_ROUTINE) sshagent_read_thread, + (void*) callback, + 0, + NULL))) + { + WLog_ERR(TAG, "CreateThread failed!"); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + *ppCallback = (IWTSVirtualChannelCallback*) callback; + + return CHANNEL_RC_OK; +} + +/** + * Callback for when the plugin is initialised + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr) +{ + SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*) pPlugin; + + sshagent->listener_callback = (SSHAGENT_LISTENER_CALLBACK*) calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK)); + + if (!sshagent->listener_callback) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection; + sshagent->listener_callback->plugin = pPlugin; + sshagent->listener_callback->channel_mgr = pChannelMgr; + + sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK"); + if (sshagent->listener_callback->agent_uds_path == NULL) + { + WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!"); + return CHANNEL_RC_INITIALIZATION_ERROR; + } + + return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0, + (IWTSListenerCallback*) sshagent->listener_callback, NULL); +} + +/** + * Callback for when the plugin is terminated + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin) +{ + SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*) pPlugin; + + free(sshagent); + + return CHANNEL_RC_OK; +} + +#ifdef BUILTIN_CHANNELS +#define DVCPluginEntry sshagent_DVCPluginEntry +#else +#define DVCPluginEntry FREERDP_API DVCPluginEntry +#endif + +/** + * Main entry point for sshagent DVC plugin + * + * @return 0 on success, otherwise a Win32 error code + */ +UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) +{ + UINT status = CHANNEL_RC_OK; + SSHAGENT_PLUGIN* sshagent; + + sshagent = (SSHAGENT_PLUGIN*) pEntryPoints->GetPlugin(pEntryPoints, "sshagent"); + + if (!sshagent) + { + sshagent = (SSHAGENT_PLUGIN*) calloc(1, sizeof(SSHAGENT_PLUGIN)); + + if (!sshagent) + { + WLog_ERR(TAG, "calloc failed!"); + return CHANNEL_RC_NO_MEMORY; + } + + sshagent->iface.Initialize = sshagent_plugin_initialize; + sshagent->iface.Connected = NULL; + sshagent->iface.Disconnected = NULL; + sshagent->iface.Terminated = sshagent_plugin_terminated; + + status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", (IWTSPlugin*) sshagent); + } + + return status; +} + +/* vim: set sw=8:ts=8:noet: */ diff --git a/channels/sshagent/client/sshagent_main.h b/channels/sshagent/client/sshagent_main.h new file mode 100644 index 000000000..135a7585a --- /dev/null +++ b/channels/sshagent/client/sshagent_main.h @@ -0,0 +1,42 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Echo Virtual Channel Extension + * + * Copyright 2013 Christian Hofstaedtler + * Copyright 2017 Ben Cohen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __SSHAGENT_MAIN_H +#define __SSHAGENT_MAIN_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include +#include + +#define DVC_TAG CHANNELS_TAG("sshagent.client") +#ifdef WITH_DEBUG_SSHAGENT +#define DEBUG_SSHAGENT(...) WLog_DBG(DVC_TAG, __VA_ARGS__) +#else +#define DEBUG_SSHAGENT(...) do { } while (0) +#endif + +#endif /* __SSHAGENT_MAIN_H */ + diff --git a/client/common/cmdline.c b/client/common/cmdline.c index 4b328b6f4..950162840 100644 --- a/client/common/cmdline.c +++ b/client/common/cmdline.c @@ -179,6 +179,7 @@ static COMMAND_LINE_ARGUMENT_A args[] = { "sound", COMMAND_LINE_VALUE_OPTIONAL, "[sys:,][dev:,][format:,][rate:,][channel:,][latency:,][quality:]", NULL, NULL, -1, "audio", "Audio output (sound)" }, { "span", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, "Span screen over multiple monitors" }, { "spn-class", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "SPN authentication service class" }, + { "ssh-agent", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "ssh-agent", "SSH Agent forwarding channel" }, { "t", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, "title", "Window title" }, { "themes", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "Enable themes" }, { "tls-ciphers", COMMAND_LINE_VALUE_REQUIRED, "netmon|ma|ciphers", NULL, NULL, -1, NULL, "Allowed TLS ciphers" }, @@ -928,6 +929,10 @@ static int freerdp_client_command_line_post_filter(void* context, { settings->SupportEchoChannel = TRUE; } + CommandLineSwitchCase(arg, "ssh-agent") + { + settings->SupportSSHAgentChannel = TRUE; + } CommandLineSwitchCase(arg, "disp") { settings->SupportDisplayControl = TRUE; @@ -2904,6 +2909,17 @@ BOOL freerdp_client_load_addins(rdpChannels* channels, rdpSettings* settings) return FALSE; } + if (settings->SupportSSHAgentChannel) + { + char* p[1]; + int count; + count = 1; + p[0] = "sshagent"; + + if (!freerdp_client_add_dynamic_channel(settings, count, p)) + return FALSE; + } + if (settings->SupportDisplayControl) { char* p[1]; diff --git a/include/freerdp/client/sshagent.h b/include/freerdp/client/sshagent.h new file mode 100644 index 000000000..536408d5b --- /dev/null +++ b/include/freerdp/client/sshagent.h @@ -0,0 +1,90 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Clipboard Virtual Channel Extension + * + * Copyright 2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2017 Ben Cohen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CHANNEL_CLIENT_SSHAGENT_H +#define FREERDP_CHANNEL_CLIENT_SSHAGENT_H + +#include <freerdp/types.h> + +#include <freerdp/message.h> +#include <freerdp/channels/cliprdr.h> +#include <freerdp/freerdp.h> + +typedef struct _sshagent_client_context +{ + int ProtocolVersion; + int MaxConnections; +} SSHAgentClientContext; + + +/* + * The channel is defined by the sshagent channel in xrdp as follows. + * + * Server to client commands + * ------------------------- + * + * Capabilities (at start of channel stream): + * + * INT32 SA_TAG_CAPABILITY + * INT32 SSHAGENT_CHAN_PROT_VERSION := 1 + * INT32 SSHAGENT_MAX_CONNECTIONS + * + * Open connection: + * + * INT32 SA_TAG_OPEN + * INT32 Connection id (0, ..., SSHAGENT_MAX_CONNECTIONS - 1) + * + * Send data: + * + * INT32 SA_TAG_WRITE + * INT32 Connection id (0, ..., SSHAGENT_MAX_CONNECTIONS - 1) + * INT32 Data length + * DATA ... + * + * Close connection: + * + * INT32 SA_TAG_CLOSE + * INT32 Connection id (0, ..., SSHAGENT_MAX_CONNECTIONS - 1) + * + * Client to server commands + * ------------------------- + * + * Capabilities (in reply to server capabilities): + * + * INT32 SA_TAG_CAPABILITY + * INT32 SSHAGENT_CHAN_PROT_VERSION := 1 + * INT32 SSHAGENT_MAX_CONNECTIONS + * + * Send data: + * + * INT32 SA_TAG_WRITE + * INT32 Connection id (0, ..., SSHAGENT_MAX_CONNECTIONS - 1) + * INT32 Data length + * DATA ... + * + * Close connection (abnormal): + * + * INT32 SA_TAG_CLOSE + * INT32 Connection id (0, ..., SSHAGENT_MAX_CONNECTIONS - 1) + */ + +#endif /* FREERDP_CHANNEL_CLIENT_SSHAGENT_H */ diff --git a/include/freerdp/settings.h b/include/freerdp/settings.h index d6af94a99..2f378dea7 100644 --- a/include/freerdp/settings.h +++ b/include/freerdp/settings.h @@ -809,6 +809,7 @@ typedef struct _RDPDR_PARALLEL RDPDR_PARALLEL; #define FreeRDP_SupportEchoChannel 5184 #define FreeRDP_SupportDisplayControl 5185 #define FreeRDP_SupportGeometryTracking 5186 +#define FreeRDP_SupportSSHAgentChannel 5187 /** * FreeRDP Settings Data Structure @@ -1422,7 +1423,8 @@ struct rdp_settings ALIGN64 BOOL SupportEchoChannel; /* 5184 */ ALIGN64 BOOL SupportDisplayControl; /* 5185 */ ALIGN64 BOOL SupportGeometryTracking; /* 5186 */ - UINT64 padding5312[5312 - 5187]; /* 5187 */ + ALIGN64 BOOL SupportSSHAgentChannel; /* 5187 */ + UINT64 padding5312[5312 - 5188]; /* 5188 */ /** * WARNING: End of ABI stable zone!