Coverage Report

Created: 2025-07-01 06:46

/src/FreeRDP/channels/sshagent/client/sshagent_main.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * FreeRDP: A Remote Desktop Protocol Implementation
3
 * SSH Agent Virtual Channel Extension
4
 *
5
 * Copyright 2013 Christian Hofstaedtler
6
 * Copyright 2015 Thincast Technologies GmbH
7
 * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
8
 * Copyright 2017 Ben Cohen
9
 *
10
 * Licensed under the Apache License, Version 2.0 (the "License");
11
 * you may not use this file except in compliance with the License.
12
 * You may obtain a copy of the License at
13
 *
14
 *     http://www.apache.org/licenses/LICENSE-2.0
15
 *
16
 * Unless required by applicable law or agreed to in writing, software
17
 * distributed under the License is distributed on an "AS IS" BASIS,
18
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
 * See the License for the specific language governing permissions and
20
 * limitations under the License.
21
 */
22
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/assert.h>
51
#include <winpr/synch.h>
52
#include <winpr/thread.h>
53
#include <winpr/stream.h>
54
55
#include "sshagent_main.h"
56
57
#include <freerdp/freerdp.h>
58
#include <freerdp/client/channels.h>
59
#include <freerdp/channels/log.h>
60
61
#define TAG CHANNELS_TAG("sshagent.client")
62
63
typedef struct
64
{
65
  IWTSListenerCallback iface;
66
67
  IWTSPlugin* plugin;
68
  IWTSVirtualChannelManager* channel_mgr;
69
70
  rdpContext* rdpcontext;
71
  const char* agent_uds_path;
72
} SSHAGENT_LISTENER_CALLBACK;
73
74
typedef struct
75
{
76
  GENERIC_CHANNEL_CALLBACK generic;
77
78
  rdpContext* rdpcontext;
79
  int agent_fd;
80
  HANDLE thread;
81
  CRITICAL_SECTION lock;
82
} SSHAGENT_CHANNEL_CALLBACK;
83
84
typedef struct
85
{
86
  IWTSPlugin iface;
87
88
  SSHAGENT_LISTENER_CALLBACK* listener_callback;
89
90
  rdpContext* rdpcontext;
91
} SSHAGENT_PLUGIN;
92
93
/**
94
 * Function to open the connection to the sshagent
95
 *
96
 * @return The fd on success, otherwise -1
97
 */
98
static int connect_to_sshagent(const char* udspath)
99
0
{
100
0
  WINPR_ASSERT(udspath);
101
102
0
  int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0);
103
104
0
  if (agent_fd == -1)
105
0
  {
106
0
    WLog_ERR(TAG, "Can't open Unix domain socket!");
107
0
    return -1;
108
0
  }
109
110
0
  struct sockaddr_un addr = { 0 };
111
112
0
  addr.sun_family = AF_UNIX;
113
114
0
  strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1);
115
116
0
  int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr));
117
118
0
  if (rc != 0)
119
0
  {
120
0
    WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", udspath);
121
0
    close(agent_fd);
122
0
    return -1;
123
0
  }
124
125
0
  return agent_fd;
126
0
}
127
128
/**
129
 * Entry point for thread to read from the ssh-agent socket and forward
130
 * the data to RDP
131
 *
132
 * @return NULL
133
 */
134
static DWORD WINAPI sshagent_read_thread(LPVOID data)
135
0
{
136
0
  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)data;
137
0
  WINPR_ASSERT(callback);
138
139
0
  BYTE buffer[4096] = { 0 };
140
0
  int going = 1;
141
0
  UINT status = CHANNEL_RC_OK;
142
143
0
  while (going)
144
0
  {
145
0
    const ssize_t bytes_read = read(callback->agent_fd, buffer, sizeof(buffer));
146
147
0
    if (bytes_read == 0)
148
0
    {
149
      /* Socket closed cleanly at other end */
150
0
      going = 0;
151
0
    }
152
0
    else if (bytes_read < 0)
153
0
    {
154
0
      if (errno != EINTR)
155
0
      {
156
0
        WLog_ERR(TAG, "Error reading from sshagent, errno=%d", errno);
157
0
        status = ERROR_READ_FAULT;
158
0
        going = 0;
159
0
      }
160
0
    }
161
0
    else if ((size_t)bytes_read > ULONG_MAX)
162
0
    {
163
0
      status = ERROR_READ_FAULT;
164
0
      going = 0;
165
0
    }
166
0
    else
167
0
    {
168
      /* Something read: forward to virtual channel */
169
0
      IWTSVirtualChannel* channel = callback->generic.channel;
170
0
      status = channel->Write(channel, (ULONG)bytes_read, buffer, NULL);
171
172
0
      if (status != CHANNEL_RC_OK)
173
0
      {
174
0
        going = 0;
175
0
      }
176
0
    }
177
0
  }
178
179
0
  close(callback->agent_fd);
180
181
0
  if (status != CHANNEL_RC_OK)
182
0
    setChannelError(callback->rdpcontext, status, "sshagent_read_thread reported an error");
183
184
0
  ExitThread(status);
185
0
  return status;
186
0
}
187
188
/**
189
 * Callback for data received from the RDP server; forward this to ssh-agent
190
 *
191
 * @return 0 on success, otherwise a Win32 error code
192
 */
193
static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
194
0
{
195
0
  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
196
0
  WINPR_ASSERT(callback);
197
198
0
  BYTE* pBuffer = Stream_Pointer(data);
199
0
  size_t cbSize = Stream_GetRemainingLength(data);
200
0
  BYTE* pos = pBuffer;
201
  /* Forward what we have received to the ssh agent */
202
0
  size_t bytes_to_write = cbSize;
203
0
  errno = 0;
204
205
0
  while (bytes_to_write > 0)
206
0
  {
207
0
    const ssize_t bytes_written = write(callback->agent_fd, pos, bytes_to_write);
208
209
0
    if (bytes_written < 0)
210
0
    {
211
0
      if (errno != EINTR)
212
0
      {
213
0
        WLog_ERR(TAG, "Error writing to sshagent, errno=%d", errno);
214
0
        return ERROR_WRITE_FAULT;
215
0
      }
216
0
    }
217
0
    else
218
0
    {
219
0
      bytes_to_write -= WINPR_ASSERTING_INT_CAST(size_t, bytes_written);
220
0
      pos += bytes_written;
221
0
    }
222
0
  }
223
224
  /* Consume stream */
225
0
  Stream_Seek(data, cbSize);
226
0
  return CHANNEL_RC_OK;
227
0
}
228
229
/**
230
 * Callback for when the virtual channel is closed
231
 *
232
 * @return 0 on success, otherwise a Win32 error code
233
 */
234
static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback)
235
0
{
236
0
  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
237
0
  WINPR_ASSERT(callback);
238
239
  /* Call shutdown() to wake up the read() in sshagent_read_thread(). */
240
0
  shutdown(callback->agent_fd, SHUT_RDWR);
241
0
  EnterCriticalSection(&callback->lock);
242
243
0
  if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED)
244
0
  {
245
0
    UINT error = GetLastError();
246
0
    WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error);
247
0
    return error;
248
0
  }
249
250
0
  (void)CloseHandle(callback->thread);
251
0
  LeaveCriticalSection(&callback->lock);
252
0
  DeleteCriticalSection(&callback->lock);
253
0
  free(callback);
254
0
  return CHANNEL_RC_OK;
255
0
}
256
257
/**
258
 * Callback for when a new virtual channel is opened
259
 *
260
 * @return 0 on success, otherwise a Win32 error code
261
 */
262
// NOLINTBEGIN(readability-non-const-parameter)
263
static UINT sshagent_on_new_channel_connection(IWTSListenerCallback* pListenerCallback,
264
                                               IWTSVirtualChannel* pChannel, BYTE* Data,
265
                                               BOOL* pbAccept,
266
                                               IWTSVirtualChannelCallback** ppCallback)
267
// NOLINTEND(readability-non-const-parameter)
268
0
{
269
0
  SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*)pListenerCallback;
270
0
  WINPR_UNUSED(Data);
271
0
  WINPR_UNUSED(pbAccept);
272
273
0
  SSHAGENT_CHANNEL_CALLBACK* callback =
274
0
      (SSHAGENT_CHANNEL_CALLBACK*)calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK));
275
276
0
  if (!callback)
277
0
  {
278
0
    WLog_ERR(TAG, "calloc failed!");
279
0
    return CHANNEL_RC_NO_MEMORY;
280
0
  }
281
282
  /* Now open a connection to the local ssh-agent.  Do this for each
283
   * connection to the plugin in case we mess up the agent session. */
284
0
  callback->agent_fd = connect_to_sshagent(listener_callback->agent_uds_path);
285
286
0
  if (callback->agent_fd == -1)
287
0
  {
288
0
    free(callback);
289
0
    return CHANNEL_RC_INITIALIZATION_ERROR;
290
0
  }
291
292
0
  InitializeCriticalSection(&callback->lock);
293
294
0
  GENERIC_CHANNEL_CALLBACK* generic = &callback->generic;
295
0
  generic->iface.OnDataReceived = sshagent_on_data_received;
296
0
  generic->iface.OnClose = sshagent_on_close;
297
0
  generic->plugin = listener_callback->plugin;
298
0
  generic->channel_mgr = listener_callback->channel_mgr;
299
0
  generic->channel = pChannel;
300
0
  callback->rdpcontext = listener_callback->rdpcontext;
301
0
  callback->thread = CreateThread(NULL, 0, sshagent_read_thread, (void*)callback, 0, NULL);
302
303
0
  if (!callback->thread)
304
0
  {
305
0
    WLog_ERR(TAG, "CreateThread failed!");
306
0
    DeleteCriticalSection(&callback->lock);
307
0
    free(callback);
308
0
    return CHANNEL_RC_INITIALIZATION_ERROR;
309
0
  }
310
311
0
  *ppCallback = (IWTSVirtualChannelCallback*)callback;
312
0
  return CHANNEL_RC_OK;
313
0
}
314
315
/**
316
 * Callback for when the plugin is initialised
317
 *
318
 * @return 0 on success, otherwise a Win32 error code
319
 */
320
static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr)
321
0
{
322
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
323
0
  WINPR_ASSERT(sshagent);
324
0
  WINPR_ASSERT(pChannelMgr);
325
326
0
  sshagent->listener_callback =
327
0
      (SSHAGENT_LISTENER_CALLBACK*)calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK));
328
329
0
  if (!sshagent->listener_callback)
330
0
  {
331
0
    WLog_ERR(TAG, "calloc failed!");
332
0
    return CHANNEL_RC_NO_MEMORY;
333
0
  }
334
335
0
  sshagent->listener_callback->rdpcontext = sshagent->rdpcontext;
336
0
  sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection;
337
0
  sshagent->listener_callback->plugin = pPlugin;
338
0
  sshagent->listener_callback->channel_mgr = pChannelMgr;
339
  // NOLINTNEXTLINE(concurrency-mt-unsafe)
340
0
  sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK");
341
342
0
  if (sshagent->listener_callback->agent_uds_path == NULL)
343
0
  {
344
0
    WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!");
345
0
    free(sshagent->listener_callback);
346
0
    sshagent->listener_callback = NULL;
347
0
    return CHANNEL_RC_INITIALIZATION_ERROR;
348
0
  }
349
350
0
  return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0,
351
0
                                     (IWTSListenerCallback*)sshagent->listener_callback, NULL);
352
0
}
353
354
/**
355
 * Callback for when the plugin is terminated
356
 *
357
 * @return 0 on success, otherwise a Win32 error code
358
 */
359
static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin)
360
0
{
361
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
362
0
  free(sshagent);
363
0
  return CHANNEL_RC_OK;
364
0
}
365
366
/**
367
 * Main entry point for sshagent DVC plugin
368
 *
369
 * @return 0 on success, otherwise a Win32 error code
370
 */
371
FREERDP_ENTRY_POINT(UINT VCAPITYPE sshagent_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
372
0
{
373
0
  UINT status = CHANNEL_RC_OK;
374
375
0
  WINPR_ASSERT(pEntryPoints);
376
377
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "sshagent");
378
379
0
  if (!sshagent)
380
0
  {
381
0
    sshagent = (SSHAGENT_PLUGIN*)calloc(1, sizeof(SSHAGENT_PLUGIN));
382
383
0
    if (!sshagent)
384
0
    {
385
0
      WLog_ERR(TAG, "calloc failed!");
386
0
      return CHANNEL_RC_NO_MEMORY;
387
0
    }
388
389
0
    sshagent->iface.Initialize = sshagent_plugin_initialize;
390
0
    sshagent->iface.Connected = NULL;
391
0
    sshagent->iface.Disconnected = NULL;
392
0
    sshagent->iface.Terminated = sshagent_plugin_terminated;
393
0
    sshagent->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints);
394
0
    status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", &sshagent->iface);
395
0
  }
396
397
0
  return status;
398
0
}