FreeRDP
sshagent_main.c
1 
23 /*
24  * sshagent_main.c: DVC plugin to forward queries from RDP to the ssh-agent
25  *
26  * This relays data to and from an ssh-agent program equivalent running on the
27  * RDP server to an ssh-agent running locally. Unlike the normal ssh-agent,
28  * which sends data over an SSH channel, the data is send over an RDP dynamic
29  * virtual channel.
30  *
31  * protocol specification:
32  * Forward data verbatim over RDP dynamic virtual channel named "sshagent"
33  * between a ssh client on the xrdp server and the real ssh-agent where
34  * the RDP client is running. Each connection by a separate client to
35  * xrdp-ssh-agent gets a separate DVC invocation.
36  */
37 
38 #include <freerdp/config.h>
39 
40 #include <stdio.h>
41 #include <stdlib.h>
42 #include <sys/types.h>
43 #include <sys/socket.h>
44 #include <sys/un.h>
45 #include <pwd.h>
46 #include <unistd.h>
47 #include <errno.h>
48 
49 #include <winpr/crt.h>
50 #include <winpr/synch.h>
51 #include <winpr/thread.h>
52 #include <winpr/stream.h>
53 
54 #include "sshagent_main.h"
55 
56 #include <freerdp/freerdp.h>
57 #include <freerdp/client/channels.h>
58 #include <freerdp/channels/log.h>
59 
60 #define TAG CHANNELS_TAG("sshagent.client")
61 
62 typedef struct
63 {
64  IWTSListenerCallback iface;
65 
66  IWTSPlugin* plugin;
67  IWTSVirtualChannelManager* channel_mgr;
68 
69  rdpContext* rdpcontext;
70  const char* agent_uds_path;
71 } SSHAGENT_LISTENER_CALLBACK;
72 
73 typedef struct
74 {
76 
77  rdpContext* rdpcontext;
78  int agent_fd;
79  HANDLE thread;
80  CRITICAL_SECTION lock;
81 } SSHAGENT_CHANNEL_CALLBACK;
82 
83 typedef struct
84 {
85  IWTSPlugin iface;
86 
87  SSHAGENT_LISTENER_CALLBACK* listener_callback;
88 
89  rdpContext* rdpcontext;
90 } SSHAGENT_PLUGIN;
91 
97 static int connect_to_sshagent(const char* udspath)
98 {
99  int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0);
100 
101  if (agent_fd == -1)
102  {
103  WLog_ERR(TAG, "Can't open Unix domain socket!");
104  return -1;
105  }
106 
107  struct sockaddr_un addr = { 0 };
108 
109  addr.sun_family = AF_UNIX;
110 
111  strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1);
112 
113  int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr));
114 
115  if (rc != 0)
116  {
117  WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", udspath);
118  close(agent_fd);
119  return -1;
120  }
121 
122  return agent_fd;
123 }
124 
131 static DWORD WINAPI sshagent_read_thread(LPVOID data)
132 {
133  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)data;
134  BYTE buffer[4096] = { 0 };
135  int going = 1;
136  UINT status = CHANNEL_RC_OK;
137 
138  while (going)
139  {
140  const ssize_t bytes_read = read(callback->agent_fd, buffer, sizeof(buffer));
141 
142  if (bytes_read == 0)
143  {
144  /* Socket closed cleanly at other end */
145  going = 0;
146  }
147  else if (bytes_read < 0)
148  {
149  if (errno != EINTR)
150  {
151  WLog_ERR(TAG, "Error reading from sshagent, errno=%d", errno);
152  status = ERROR_READ_FAULT;
153  going = 0;
154  }
155  }
156  else if ((size_t)bytes_read > ULONG_MAX)
157  {
158  status = ERROR_READ_FAULT;
159  going = 0;
160  }
161  else
162  {
163  /* Something read: forward to virtual channel */
164  IWTSVirtualChannel* channel = callback->generic.channel;
165  status = channel->Write(channel, (ULONG)bytes_read, buffer, NULL);
166 
167  if (status != CHANNEL_RC_OK)
168  {
169  going = 0;
170  }
171  }
172  }
173 
174  close(callback->agent_fd);
175 
176  if (status != CHANNEL_RC_OK)
177  setChannelError(callback->rdpcontext, status, "sshagent_read_thread reported an error");
178 
179  ExitThread(status);
180  return status;
181 }
182 
188 static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
189 {
190  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
191  BYTE* pBuffer = Stream_Pointer(data);
192  size_t cbSize = Stream_GetRemainingLength(data);
193  BYTE* pos = pBuffer;
194  /* Forward what we have received to the ssh agent */
195  size_t bytes_to_write = cbSize;
196  errno = 0;
197 
198  while (bytes_to_write > 0)
199  {
200  const ssize_t bytes_written = write(callback->agent_fd, pos, bytes_to_write);
201 
202  if (bytes_written < 0)
203  {
204  if (errno != EINTR)
205  {
206  WLog_ERR(TAG, "Error writing to sshagent, errno=%d", errno);
207  return ERROR_WRITE_FAULT;
208  }
209  }
210  else
211  {
212  bytes_to_write -= bytes_written;
213  pos += bytes_written;
214  }
215  }
216 
217  /* Consume stream */
218  Stream_Seek(data, cbSize);
219  return CHANNEL_RC_OK;
220 }
221 
227 static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback)
228 {
229  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
230  /* Call shutdown() to wake up the read() in sshagent_read_thread(). */
231  shutdown(callback->agent_fd, SHUT_RDWR);
232  EnterCriticalSection(&callback->lock);
233 
234  if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED)
235  {
236  UINT error = GetLastError();
237  WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error);
238  return error;
239  }
240 
241  (void)CloseHandle(callback->thread);
242  LeaveCriticalSection(&callback->lock);
243  DeleteCriticalSection(&callback->lock);
244  free(callback);
245  return CHANNEL_RC_OK;
246 }
247 
253 static UINT sshagent_on_new_channel_connection(IWTSListenerCallback* pListenerCallback,
254  IWTSVirtualChannel* pChannel, BYTE* Data,
255  BOOL* pbAccept,
256  IWTSVirtualChannelCallback** ppCallback)
257 {
258  SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*)pListenerCallback;
259  SSHAGENT_CHANNEL_CALLBACK* callback =
260  (SSHAGENT_CHANNEL_CALLBACK*)calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK));
261 
262  if (!callback)
263  {
264  WLog_ERR(TAG, "calloc failed!");
265  return CHANNEL_RC_NO_MEMORY;
266  }
267 
268  /* Now open a connection to the local ssh-agent. Do this for each
269  * connection to the plugin in case we mess up the agent session. */
270  callback->agent_fd = connect_to_sshagent(listener_callback->agent_uds_path);
271 
272  if (callback->agent_fd == -1)
273  {
274  free(callback);
275  return CHANNEL_RC_INITIALIZATION_ERROR;
276  }
277 
278  InitializeCriticalSection(&callback->lock);
279 
280  GENERIC_CHANNEL_CALLBACK* generic = &callback->generic;
281  generic->iface.OnDataReceived = sshagent_on_data_received;
282  generic->iface.OnClose = sshagent_on_close;
283  generic->plugin = listener_callback->plugin;
284  generic->channel_mgr = listener_callback->channel_mgr;
285  generic->channel = pChannel;
286  callback->rdpcontext = listener_callback->rdpcontext;
287  callback->thread = CreateThread(NULL, 0, sshagent_read_thread, (void*)callback, 0, NULL);
288 
289  if (!callback->thread)
290  {
291  WLog_ERR(TAG, "CreateThread failed!");
292  DeleteCriticalSection(&callback->lock);
293  free(callback);
294  return CHANNEL_RC_INITIALIZATION_ERROR;
295  }
296 
297  *ppCallback = (IWTSVirtualChannelCallback*)callback;
298  return CHANNEL_RC_OK;
299 }
300 
306 static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr)
307 {
308  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
309  sshagent->listener_callback =
310  (SSHAGENT_LISTENER_CALLBACK*)calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK));
311 
312  if (!sshagent->listener_callback)
313  {
314  WLog_ERR(TAG, "calloc failed!");
315  return CHANNEL_RC_NO_MEMORY;
316  }
317 
318  sshagent->listener_callback->rdpcontext = sshagent->rdpcontext;
319  sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection;
320  sshagent->listener_callback->plugin = pPlugin;
321  sshagent->listener_callback->channel_mgr = pChannelMgr;
322  sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK");
323 
324  if (sshagent->listener_callback->agent_uds_path == NULL)
325  {
326  WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!");
327  free(sshagent->listener_callback);
328  sshagent->listener_callback = NULL;
329  return CHANNEL_RC_INITIALIZATION_ERROR;
330  }
331 
332  return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0,
333  (IWTSListenerCallback*)sshagent->listener_callback, NULL);
334 }
335 
341 static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin)
342 {
343  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
344  free(sshagent);
345  return CHANNEL_RC_OK;
346 }
347 
353 FREERDP_ENTRY_POINT(UINT VCAPITYPE sshagent_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
354 {
355  UINT status = CHANNEL_RC_OK;
356  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "sshagent");
357 
358  if (!sshagent)
359  {
360  sshagent = (SSHAGENT_PLUGIN*)calloc(1, sizeof(SSHAGENT_PLUGIN));
361 
362  if (!sshagent)
363  {
364  WLog_ERR(TAG, "calloc failed!");
365  return CHANNEL_RC_NO_MEMORY;
366  }
367 
368  sshagent->iface.Initialize = sshagent_plugin_initialize;
369  sshagent->iface.Connected = NULL;
370  sshagent->iface.Disconnected = NULL;
371  sshagent->iface.Terminated = sshagent_plugin_terminated;
372  sshagent->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints);
373  status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", &sshagent->iface);
374  }
375 
376  return status;
377 }
378 
379 /* vim: set sw=8:ts=8:noet: */