1 /* GStreamer
2 * Copyright (C) 2011 Axis Communications <dev-gstreamer@axis.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20 /**
21 * SECTION:element-curlsshsink
22 * @title: curlsshsink
23 * @short_description: sink that uploads data to a server using libcurl
24 *
25 * This is a network sink that uses libcurl.
26 *
27 */
28
29 #ifdef HAVE_CONFIG_H
30 #include "config.h"
31 #endif
32
33 #include "gstcurlbasesink.h"
34 #include "gstcurlsshsink.h"
35
36 #include <curl/curl.h>
37 #include <string.h>
38 #include <stdio.h>
39
40 #ifdef G_OS_WIN32
41 #include <winsock2.h>
42 #else
43 #include <sys/socket.h>
44 #include <netinet/in.h>
45 #include <netinet/ip.h>
46 #include <netinet/tcp.h>
47 #endif
48 #include <sys/types.h>
49 #include <unistd.h>
50 #include <sys/stat.h>
51 #include <fcntl.h>
52
53 /* Default values */
54 #define GST_CAT_DEFAULT gst_curl_ssh_sink_debug
55
56 /* Plugin specific settings */
57
58 GST_DEBUG_CATEGORY_STATIC (gst_curl_ssh_sink_debug);
59
60 enum
61 {
62 PROP_0,
63 PROP_SSH_AUTH_TYPE,
64 PROP_SSH_PUB_KEYFILE,
65 PROP_SSH_PRIV_KEYFILE,
66 PROP_SSH_KEY_PASSPHRASE,
67 PROP_SSH_KNOWNHOSTS,
68 PROP_SSH_HOST_PUBLIC_KEY_MD5,
69 PROP_SSH_HOST_PUBLIC_KEY_SHA256,
70 PROP_SSH_ACCEPT_UNKNOWNHOST
71 };
72
73
74 /* curl SSH-key matching callback */
75 static gint curl_ssh_sink_sshkey_cb (CURL * easy_handle,
76 const struct curl_khkey *knownkey, const struct curl_khkey *foundkey,
77 enum curl_khmatch, void *clientp);
78
79
80 /* Object class function declarations */
81
82 static void gst_curl_ssh_sink_set_property (GObject * object, guint prop_id,
83 const GValue * value, GParamSpec * pspec);
84 static void gst_curl_ssh_sink_get_property (GObject * object, guint prop_id,
85 GValue * value, GParamSpec * pspec);
86 static void gst_curl_ssh_sink_finalize (GObject * gobject);
87 static gboolean gst_curl_ssh_sink_set_options_unlocked (GstCurlBaseSink *
88 bcsink);
89
90
91 /* private functions */
92
93 #define gst_curl_ssh_sink_parent_class parent_class
94 G_DEFINE_TYPE (GstCurlSshSink, gst_curl_ssh_sink, GST_TYPE_CURL_BASE_SINK);
95
96 /* Register the auth types with the GLib type system */
97 #define GST_TYPE_CURL_SSH_SINK_AUTH_TYPE (gst_curl_ssh_sink_auth_get_type ())
98 static GType
gst_curl_ssh_sink_auth_get_type(void)99 gst_curl_ssh_sink_auth_get_type (void)
100 {
101 static GType gtype = 0;
102
103 if (!gtype) {
104 static const GEnumValue auth_types[] = {
105 {GST_CURLSSH_AUTH_NONE, "Not allowed", "none"},
106 {GST_CURLSSH_AUTH_PUBLICKEY, "Public/private key files", "pubkey"},
107 {GST_CURLSSH_AUTH_PASSWORD, "Password authentication", "password"},
108 {0, NULL, NULL}
109 };
110 gtype = g_enum_register_static ("GstCurlSshAuthType", auth_types);
111 }
112 return gtype;
113 }
114
115 static void
gst_curl_ssh_sink_class_init(GstCurlSshSinkClass * klass)116 gst_curl_ssh_sink_class_init (GstCurlSshSinkClass * klass)
117 {
118 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
119 GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
120
121 GST_DEBUG_CATEGORY_INIT (gst_curl_ssh_sink_debug, "curlsshsink", 0,
122 "curl ssh sink element");
123
124 gst_element_class_set_static_metadata (element_class,
125 "Curl SSH sink", "Sink/Network",
126 "Upload data over SSH/SFTP using libcurl", "Sorin L. <sorin@axis.com>");
127
128 gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_curl_ssh_sink_finalize);
129
130 gobject_class->set_property = gst_curl_ssh_sink_set_property;
131 gobject_class->get_property = gst_curl_ssh_sink_get_property;
132
133 klass->set_options_unlocked = gst_curl_ssh_sink_set_options_unlocked;
134
135 g_object_class_install_property (gobject_class, PROP_SSH_AUTH_TYPE,
136 g_param_spec_enum ("ssh-auth-type", "SSH authentication type",
137 "SSH authentication method to authenticate on the SSH/SFTP server",
138 GST_TYPE_CURL_SSH_SINK_AUTH_TYPE, GST_CURLSSH_AUTH_NONE,
139 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
140
141 g_object_class_install_property (gobject_class, PROP_SSH_PUB_KEYFILE,
142 g_param_spec_string ("ssh-pub-keyfile",
143 "SSH public key file",
144 "The complete path & filename of the SSH public key file",
145 NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
146
147 g_object_class_install_property (gobject_class, PROP_SSH_PRIV_KEYFILE,
148 g_param_spec_string ("ssh-priv-keyfile",
149 "SSH private key file",
150 "The complete path & filename of the SSH private key file",
151 NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
152
153 g_object_class_install_property (gobject_class, PROP_SSH_KEY_PASSPHRASE,
154 g_param_spec_string ("ssh-key-passphrase", "Passphrase of the priv key",
155 "The passphrase used to protect the SSH private key file",
156 NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
157
158 g_object_class_install_property (gobject_class, PROP_SSH_KNOWNHOSTS,
159 g_param_spec_string ("ssh-knownhosts",
160 "SSH known hosts",
161 "The complete path & filename of the SSH 'known_hosts' file",
162 NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
163
164 g_object_class_install_property (gobject_class, PROP_SSH_HOST_PUBLIC_KEY_MD5,
165 g_param_spec_string ("ssh-host-pubkey-md5",
166 "MD5 checksum of the remote host's public key",
167 "MD5 checksum (32 hexadecimal digits, case-insensitive) of the "
168 "remote host's public key",
169 NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
170
171 #if CURL_AT_LEAST_VERSION(7, 80, 0)
172 g_object_class_install_property (gobject_class,
173 PROP_SSH_HOST_PUBLIC_KEY_SHA256,
174 g_param_spec_string ("ssh-host-pubkey-sha256",
175 "SHA256 checksum of the remote host's public key",
176 "SHA256 checksum (Base64 encoded) of the remote host's public key",
177 NULL, G_PARAM_READWRITE | GST_PARAM_CONDITIONALLY_AVAILABLE |
178 G_PARAM_STATIC_STRINGS));
179 #endif
180
181 g_object_class_install_property (gobject_class, PROP_SSH_ACCEPT_UNKNOWNHOST,
182 g_param_spec_boolean ("ssh-accept-unknownhost",
183 "SSH accept unknown host",
184 "Accept an unknown remote public host key",
185 FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
186
187 gst_type_mark_as_plugin_api (GST_TYPE_CURL_SSH_SINK_AUTH_TYPE, 0);
188 gst_type_mark_as_plugin_api (GST_TYPE_CURL_SSH_SINK, 0);
189 }
190
191 static void
gst_curl_ssh_sink_init(GstCurlSshSink * sink)192 gst_curl_ssh_sink_init (GstCurlSshSink * sink)
193 {
194 sink->ssh_auth_type = GST_CURLSSH_AUTH_NONE;
195 sink->ssh_pub_keyfile = NULL;
196 sink->ssh_priv_keyfile = NULL;
197 sink->ssh_key_passphrase = NULL;
198 sink->ssh_knownhosts = NULL;
199 sink->ssh_host_public_key_md5 = NULL;
200 sink->ssh_host_public_key_sha256 = NULL;
201 sink->ssh_accept_unknownhost = FALSE;
202 }
203
204 static void
gst_curl_ssh_sink_finalize(GObject * gobject)205 gst_curl_ssh_sink_finalize (GObject * gobject)
206 {
207 GstCurlSshSink *this = GST_CURL_SSH_SINK (gobject);
208
209 GST_DEBUG ("finalizing curlsshsink");
210
211 g_free (this->ssh_pub_keyfile);
212 g_free (this->ssh_priv_keyfile);
213 g_free (this->ssh_key_passphrase);
214 g_free (this->ssh_knownhosts);
215 g_free (this->ssh_host_public_key_md5);
216 g_free (this->ssh_host_public_key_sha256);
217
218 G_OBJECT_CLASS (parent_class)->finalize (gobject);
219 }
220
221 static void
gst_curl_ssh_sink_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)222 gst_curl_ssh_sink_set_property (GObject * object, guint prop_id,
223 const GValue * value, GParamSpec * pspec)
224 {
225 GstCurlSshSink *sink;
226 GstState cur_state;
227
228 g_return_if_fail (GST_IS_CURL_SSH_SINK (object));
229 sink = GST_CURL_SSH_SINK (object);
230
231 gst_element_get_state (GST_ELEMENT (sink), &cur_state, NULL, 0);
232
233 if (cur_state == GST_STATE_PLAYING || cur_state == GST_STATE_PAUSED) {
234 return;
235 }
236
237 GST_OBJECT_LOCK (sink);
238 switch (prop_id) {
239 case PROP_SSH_AUTH_TYPE:
240 sink->ssh_auth_type = g_value_get_enum (value);
241 GST_DEBUG_OBJECT (sink, "ssh_auth_type set to %d", sink->ssh_auth_type);
242 break;
243
244 case PROP_SSH_PUB_KEYFILE:
245 g_free (sink->ssh_pub_keyfile);
246 sink->ssh_pub_keyfile = g_value_dup_string (value);
247 GST_DEBUG_OBJECT (sink, "ssh_pub_keyfile set to %s",
248 sink->ssh_pub_keyfile);
249 break;
250
251 case PROP_SSH_PRIV_KEYFILE:
252 g_free (sink->ssh_priv_keyfile);
253 sink->ssh_priv_keyfile = g_value_dup_string (value);
254 GST_DEBUG_OBJECT (sink, "ssh_priv_keyfile set to %s",
255 sink->ssh_priv_keyfile);
256 break;
257
258 case PROP_SSH_KEY_PASSPHRASE:
259 g_free (sink->ssh_key_passphrase);
260 sink->ssh_key_passphrase = g_value_dup_string (value);
261 GST_DEBUG_OBJECT (sink, "ssh_key_passphrase set to %s",
262 sink->ssh_key_passphrase);
263 break;
264
265 case PROP_SSH_KNOWNHOSTS:
266 g_free (sink->ssh_knownhosts);
267 sink->ssh_knownhosts = g_value_dup_string (value);
268 GST_DEBUG_OBJECT (sink, "ssh_knownhosts set to %s", sink->ssh_knownhosts);
269 break;
270
271 case PROP_SSH_HOST_PUBLIC_KEY_MD5:
272 g_free (sink->ssh_host_public_key_md5);
273 sink->ssh_host_public_key_md5 = g_value_dup_string (value);
274 GST_DEBUG_OBJECT (sink, "ssh_host_public_key_md5 set to %s",
275 sink->ssh_host_public_key_md5);
276 break;
277
278 case PROP_SSH_HOST_PUBLIC_KEY_SHA256:
279 g_free (sink->ssh_host_public_key_sha256);
280 sink->ssh_host_public_key_sha256 = g_value_dup_string (value);
281 GST_DEBUG_OBJECT (sink, "ssh_host_public_key_sha256 set to %s",
282 sink->ssh_host_public_key_sha256);
283 break;
284
285 case PROP_SSH_ACCEPT_UNKNOWNHOST:
286 sink->ssh_accept_unknownhost = g_value_get_boolean (value);
287 GST_DEBUG_OBJECT (sink, "ssh_accept_unknownhost set to %d",
288 sink->ssh_accept_unknownhost);
289 break;
290
291 default:
292 GST_DEBUG_OBJECT (sink, "invalid property id %d", prop_id);
293 break;
294 }
295 GST_OBJECT_UNLOCK (sink);
296 }
297
298 static void
gst_curl_ssh_sink_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)299 gst_curl_ssh_sink_get_property (GObject * object, guint prop_id,
300 GValue * value, GParamSpec * pspec)
301 {
302 GstCurlSshSink *sink;
303
304 g_return_if_fail (GST_IS_CURL_SSH_SINK (object));
305 sink = GST_CURL_SSH_SINK (object);
306
307 switch (prop_id) {
308 case PROP_SSH_AUTH_TYPE:
309 g_value_set_enum (value, sink->ssh_auth_type);
310 break;
311
312 case PROP_SSH_PUB_KEYFILE:
313 g_value_set_string (value, sink->ssh_pub_keyfile);
314 break;
315
316 case PROP_SSH_PRIV_KEYFILE:
317 g_value_set_string (value, sink->ssh_priv_keyfile);
318 break;
319
320 case PROP_SSH_KEY_PASSPHRASE:
321 g_value_set_string (value, sink->ssh_key_passphrase);
322 break;
323
324 case PROP_SSH_KNOWNHOSTS:
325 g_value_set_string (value, sink->ssh_knownhosts);
326 break;
327
328 case PROP_SSH_HOST_PUBLIC_KEY_MD5:
329 g_value_set_string (value, sink->ssh_host_public_key_md5);
330 break;
331
332 case PROP_SSH_HOST_PUBLIC_KEY_SHA256:
333 g_value_set_string (value, sink->ssh_host_public_key_sha256);
334 break;
335
336 case PROP_SSH_ACCEPT_UNKNOWNHOST:
337 g_value_set_boolean (value, sink->ssh_accept_unknownhost);
338 break;
339
340 default:
341 GST_DEBUG_OBJECT (sink, "invalid property id");
342 break;
343 }
344 }
345
346 static gboolean
gst_curl_ssh_sink_set_options_unlocked(GstCurlBaseSink * bcsink)347 gst_curl_ssh_sink_set_options_unlocked (GstCurlBaseSink * bcsink)
348 {
349 GstCurlSshSink *sink = GST_CURL_SSH_SINK (bcsink);
350 CURLcode curl_err = CURLE_OK;
351
352 /* set SSH specific options here */
353 if (sink->ssh_pub_keyfile) {
354 if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_PUBLIC_KEYFILE,
355 sink->ssh_pub_keyfile)) != CURLE_OK) {
356 bcsink->error = g_strdup_printf ("failed to set public key file: %s",
357 curl_easy_strerror (curl_err));
358 return FALSE;
359 }
360 }
361
362 if (sink->ssh_priv_keyfile) {
363 if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_PRIVATE_KEYFILE,
364 sink->ssh_priv_keyfile)) != CURLE_OK) {
365 bcsink->error = g_strdup_printf ("failed to set private key file: %s",
366 curl_easy_strerror (curl_err));
367 return FALSE;
368 }
369 }
370
371 if (sink->ssh_knownhosts) {
372 if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KNOWNHOSTS,
373 sink->ssh_knownhosts)) != CURLE_OK) {
374 bcsink->error = g_strdup_printf ("failed to set known_hosts file: %s",
375 curl_easy_strerror (curl_err));
376 return FALSE;
377 }
378 }
379
380 if (sink->ssh_host_public_key_md5) {
381 /* libcurl is freaking tricky. If the input string is not exactly 32
382 * hexdigits long it silently ignores CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 and
383 * performs the transfer without authenticating the server! */
384 if (strlen (sink->ssh_host_public_key_md5) != 32) {
385 bcsink->error = g_strdup ("MD5-hash string has invalid length, "
386 "must be exactly 32 hexdigits!");
387 return FALSE;
388 }
389
390 if ((curl_err =
391 curl_easy_setopt (bcsink->curl, CURLOPT_SSH_HOST_PUBLIC_KEY_MD5,
392 sink->ssh_host_public_key_md5)) != CURLE_OK) {
393 bcsink->error = g_strdup_printf ("failed to set remote host's public "
394 "key MD5: %s", curl_easy_strerror (curl_err));
395 return FALSE;
396 }
397 }
398 #if CURL_AT_LEAST_VERSION(7, 80, 0)
399 if (sink->ssh_host_public_key_sha256) {
400 if ((curl_err =
401 curl_easy_setopt (bcsink->curl, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
402 sink->ssh_host_public_key_sha256)) != CURLE_OK) {
403 bcsink->error = g_strdup_printf ("failed to set remote host's public "
404 "key SHA256: %s", curl_easy_strerror (curl_err));
405 return FALSE;
406 }
407 }
408 #endif
409
410 /* make sure we only accept PASSWORD or PUBLICKEY auth methods
411 * (can be extended later) */
412 if (sink->ssh_auth_type == GST_CURLSSH_AUTH_PASSWORD ||
413 sink->ssh_auth_type == GST_CURLSSH_AUTH_PUBLICKEY) {
414
415 /* set the SSH_AUTH_TYPE */
416 if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_AUTH_TYPES,
417 sink->ssh_auth_type)) != CURLE_OK) {
418 bcsink->error = g_strdup_printf ("failed to set authentication type: %s",
419 curl_easy_strerror (curl_err));
420 return FALSE;
421 }
422
423 /* if key authentication -> provide the private key passphrase as well */
424 if (sink->ssh_auth_type == GST_CURLSSH_AUTH_PUBLICKEY) {
425 if (sink->ssh_key_passphrase) {
426 if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_KEYPASSWD,
427 sink->ssh_key_passphrase)) != CURLE_OK) {
428 bcsink->error = g_strdup_printf ("failed to set private key "
429 "passphrase: %s", curl_easy_strerror (curl_err));
430 return FALSE;
431 }
432 } else {
433 /* The user did not provide the passphrase for the private key.
434 * This can still be a valid situation, if (s)he chose not to
435 * protect the private key with a passphrase - but not recommended! */
436 GST_WARNING_OBJECT (sink, "Warning: key authentication chosen but "
437 "'ssh-key-passphrase' not provided!");
438 }
439 }
440
441 } else {
442 bcsink->error = g_strdup_printf ("Error: unsupported authentication type: "
443 "%d.", sink->ssh_auth_type);
444 return FALSE;
445 }
446
447 /* Install the SSH_KEYFUNCTION callback...
448 * IMPORTANT: this callback gets called only if CURLOPT_SSH_KNOWNHOSTS
449 * is also set! */
450 if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KEYFUNCTION,
451 curl_ssh_sink_sshkey_cb)) != CURLE_OK) {
452 bcsink->error = g_strdup_printf ("failed to set SSH_KEYFUNCTION callback: "
453 "%s", curl_easy_strerror (curl_err));
454 return FALSE;
455 }
456 /* SSH_KEYFUNCTION callback successfully installed so go on and
457 * set the '*clientp' parameter as well */
458 if ((curl_err =
459 curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KEYDATA,
460 sink)) != CURLE_OK) {
461 bcsink->error = g_strdup_printf ("failed to set CURLOPT_SSH_KEYDATA: %s",
462 curl_easy_strerror (curl_err));
463 return FALSE;
464 }
465
466 return TRUE;
467 }
468
469
470 /* A 'curl_sshkey_cb' callback function. It gets called when the known_host
471 * matching has been done, to allow the application to act and decide for
472 * libcurl how to proceed.
473 * The callback will only be called if CURLOPT_SSH_KNOWNHOSTS is also set!
474 * NOTE:
475 * * use CURLOPT_SSH_KEYFUNCTION to install the callback func
476 * * use CURLOPT_SSH_KEYDATA to pass in the actual "*clientp"
477 */
478 static gint
curl_ssh_sink_sshkey_cb(CURL * easy_handle,const struct curl_khkey * knownkey,const struct curl_khkey * foundkey,enum curl_khmatch match,void * clientp)479 curl_ssh_sink_sshkey_cb (CURL * easy_handle, /* easy handle */
480 const struct curl_khkey *knownkey, /* known - key from known_hosts */
481 const struct curl_khkey *foundkey, /* found - key from remote end */
482 enum curl_khmatch match, /* libcurl's view on the keys */
483 void *clientp)
484 {
485 GstCurlSshSink *sink = (GstCurlSshSink *) clientp;
486
487 /* the default action to be taken upon pub key matching */
488 guint res_action = CURLKHSTAT_REJECT;
489
490 switch (match) {
491 case CURLKHMATCH_OK:
492 res_action = CURLKHSTAT_FINE;
493 GST_INFO_OBJECT (sink,
494 "Remote public host key is matching known_hosts, OK to proceed.");
495 break;
496
497 case CURLKHMATCH_MISMATCH:
498 GST_WARNING_OBJECT (sink,
499 "Remote public host key mismatch in known_hosts, aborting "
500 "connection.");
501 /* Reject the connection. The old mismatching key has to be manually
502 * removed from 'known_hosts' before being able to connect again to
503 * the respective host. */
504 break;
505
506 case CURLKHMATCH_MISSING:
507 GST_OBJECT_LOCK (sink);
508 if (sink->ssh_accept_unknownhost == TRUE) {
509 /* the key was not found in known_hosts but the user chose to
510 * accept it */
511 res_action = CURLKHSTAT_FINE_ADD_TO_FILE;
512 GST_INFO_OBJECT (sink, "Accepting and adding new public host key to "
513 "known_hosts.");
514 } else {
515 /* the key was not found in known_hosts and the user chose not
516 * to accept connections to unknown hosts */
517 GST_WARNING_OBJECT (sink,
518 "Remote public host key is unknown, rejecting connection.");
519 }
520 GST_OBJECT_UNLOCK (sink);
521 break;
522
523 default:
524 /* something went wrong, we got some bogus key match result */
525 GST_CURL_BASE_SINK (sink)->error =
526 g_strdup ("libcurl internal error during known_host matching");
527 break;
528 }
529
530 return res_action;
531 }
532