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  * @see_also:
25  *
26  * This is a network sink that uses libcurl.
27  *
28  */
29 
30 #ifdef HAVE_CONFIG_H
31 #include "config.h"
32 #endif
33 
34 #include "gstcurlbasesink.h"
35 #include "gstcurlsshsink.h"
36 
37 #include <curl/curl.h>
38 #include <string.h>
39 #include <stdio.h>
40 
41 #ifdef G_OS_WIN32
42 #include <winsock2.h>
43 #else
44 #include <sys/socket.h>
45 #include <netinet/in.h>
46 #include <netinet/ip.h>
47 #include <netinet/tcp.h>
48 #endif
49 #include <sys/types.h>
50 #include <unistd.h>
51 #include <sys/stat.h>
52 #include <fcntl.h>
53 
54 /* Default values */
55 #define GST_CAT_DEFAULT    gst_curl_ssh_sink_debug
56 
57 /* Plugin specific settings */
58 
59 GST_DEBUG_CATEGORY_STATIC (gst_curl_ssh_sink_debug);
60 
61 enum
62 {
63   PROP_0,
64   PROP_SSH_AUTH_TYPE,
65   PROP_SSH_PUB_KEYFILE,
66   PROP_SSH_PRIV_KEYFILE,
67   PROP_SSH_KEY_PASSPHRASE,
68   PROP_SSH_KNOWNHOSTS,
69   PROP_SSH_HOST_PUBLIC_KEY_MD5,
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_DEBUG_OBJECT (klass, "class_init");
125 
126   gst_element_class_set_static_metadata (element_class,
127       "Curl SSH sink", "Sink/Network",
128       "Upload data over SSH/SFTP using libcurl", "Sorin L. <sorin@axis.com>");
129 
130   gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_curl_ssh_sink_finalize);
131 
132   gobject_class->set_property = gst_curl_ssh_sink_set_property;
133   gobject_class->get_property = gst_curl_ssh_sink_get_property;
134 
135   klass->set_options_unlocked = gst_curl_ssh_sink_set_options_unlocked;
136 
137   g_object_class_install_property (gobject_class, PROP_SSH_AUTH_TYPE,
138       g_param_spec_enum ("ssh-auth-type", "SSH authentication type",
139           "SSH authentication method to authenticate on the SSH/SFTP server",
140           GST_TYPE_CURL_SSH_SINK_AUTH_TYPE, GST_CURLSSH_AUTH_NONE,
141           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
142 
143   g_object_class_install_property (gobject_class, PROP_SSH_PUB_KEYFILE,
144       g_param_spec_string ("ssh-pub-keyfile",
145           "SSH public key file",
146           "The complete path & filename of the SSH public key file",
147           NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
148 
149   g_object_class_install_property (gobject_class, PROP_SSH_PRIV_KEYFILE,
150       g_param_spec_string ("ssh-priv-keyfile",
151           "SSH private key file",
152           "The complete path & filename of the SSH private key file",
153           NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
154 
155   g_object_class_install_property (gobject_class, PROP_SSH_KEY_PASSPHRASE,
156       g_param_spec_string ("ssh-key-passphrase", "Passphrase of the priv key",
157           "The passphrase used to protect the SSH private key file",
158           NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
159 
160   g_object_class_install_property (gobject_class, PROP_SSH_KNOWNHOSTS,
161       g_param_spec_string ("ssh-knownhosts",
162           "SSH known hosts",
163           "The complete path & filename of the SSH 'known_hosts' file",
164           NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
165 
166   g_object_class_install_property (gobject_class, PROP_SSH_HOST_PUBLIC_KEY_MD5,
167       g_param_spec_string ("ssh-host-pubkey-md5",
168           "MD5 checksum of the remote host's public key",
169           "MD5 checksum (32 hexadecimal digits, case-insensitive) of the "
170           "remote host's public key",
171           NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
172 
173   g_object_class_install_property (gobject_class, PROP_SSH_ACCEPT_UNKNOWNHOST,
174       g_param_spec_boolean ("ssh-accept-unknownhost",
175           "SSH accept unknown host",
176           "Accept an unknown remote public host key",
177           FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
178 }
179 
180 static void
gst_curl_ssh_sink_init(GstCurlSshSink * sink)181 gst_curl_ssh_sink_init (GstCurlSshSink * sink)
182 {
183   sink->ssh_auth_type = GST_CURLSSH_AUTH_NONE;
184   sink->ssh_pub_keyfile = NULL;
185   sink->ssh_priv_keyfile = NULL;
186   sink->ssh_key_passphrase = NULL;
187   sink->ssh_knownhosts = NULL;
188   sink->ssh_host_public_key_md5 = NULL;
189   sink->ssh_accept_unknownhost = FALSE;
190 }
191 
192 static void
gst_curl_ssh_sink_finalize(GObject * gobject)193 gst_curl_ssh_sink_finalize (GObject * gobject)
194 {
195   GstCurlSshSink *this = GST_CURL_SSH_SINK (gobject);
196 
197   GST_DEBUG ("finalizing curlsshsink");
198 
199   g_free (this->ssh_pub_keyfile);
200   g_free (this->ssh_priv_keyfile);
201   g_free (this->ssh_key_passphrase);
202   g_free (this->ssh_knownhosts);
203   g_free (this->ssh_host_public_key_md5);
204 
205   G_OBJECT_CLASS (parent_class)->finalize (gobject);
206 }
207 
208 static void
gst_curl_ssh_sink_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)209 gst_curl_ssh_sink_set_property (GObject * object, guint prop_id,
210     const GValue * value, GParamSpec * pspec)
211 {
212   GstCurlSshSink *sink;
213   GstState cur_state;
214 
215   g_return_if_fail (GST_IS_CURL_SSH_SINK (object));
216   sink = GST_CURL_SSH_SINK (object);
217 
218   gst_element_get_state (GST_ELEMENT (sink), &cur_state, NULL, 0);
219 
220   if (cur_state == GST_STATE_PLAYING || cur_state == GST_STATE_PAUSED) {
221     return;
222   }
223 
224   GST_OBJECT_LOCK (sink);
225   switch (prop_id) {
226     case PROP_SSH_AUTH_TYPE:
227       sink->ssh_auth_type = g_value_get_enum (value);
228       GST_DEBUG_OBJECT (sink, "ssh_auth_type set to %d", sink->ssh_auth_type);
229       break;
230 
231     case PROP_SSH_PUB_KEYFILE:
232       g_free (sink->ssh_pub_keyfile);
233       sink->ssh_pub_keyfile = g_value_dup_string (value);
234       GST_DEBUG_OBJECT (sink, "ssh_pub_keyfile set to %s",
235           sink->ssh_pub_keyfile);
236       break;
237 
238     case PROP_SSH_PRIV_KEYFILE:
239       g_free (sink->ssh_priv_keyfile);
240       sink->ssh_priv_keyfile = g_value_dup_string (value);
241       GST_DEBUG_OBJECT (sink, "ssh_priv_keyfile set to %s",
242           sink->ssh_priv_keyfile);
243       break;
244 
245     case PROP_SSH_KEY_PASSPHRASE:
246       g_free (sink->ssh_key_passphrase);
247       sink->ssh_key_passphrase = g_value_dup_string (value);
248       GST_DEBUG_OBJECT (sink, "ssh_key_passphrase set to %s",
249           sink->ssh_key_passphrase);
250       break;
251 
252     case PROP_SSH_KNOWNHOSTS:
253       g_free (sink->ssh_knownhosts);
254       sink->ssh_knownhosts = g_value_dup_string (value);
255       GST_DEBUG_OBJECT (sink, "ssh_knownhosts set to %s", sink->ssh_knownhosts);
256       break;
257 
258     case PROP_SSH_HOST_PUBLIC_KEY_MD5:
259       g_free (sink->ssh_host_public_key_md5);
260       sink->ssh_host_public_key_md5 = g_value_dup_string (value);
261       GST_DEBUG_OBJECT (sink, "ssh_host_public_key_md5 set to %s",
262           sink->ssh_host_public_key_md5);
263       break;
264 
265     case PROP_SSH_ACCEPT_UNKNOWNHOST:
266       sink->ssh_accept_unknownhost = g_value_get_boolean (value);
267       GST_DEBUG_OBJECT (sink, "ssh_accept_unknownhost set to %d",
268           sink->ssh_accept_unknownhost);
269       break;
270 
271     default:
272       GST_DEBUG_OBJECT (sink, "invalid property id %d", prop_id);
273       break;
274   }
275   GST_OBJECT_UNLOCK (sink);
276 }
277 
278 static void
gst_curl_ssh_sink_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)279 gst_curl_ssh_sink_get_property (GObject * object, guint prop_id,
280     GValue * value, GParamSpec * pspec)
281 {
282   GstCurlSshSink *sink;
283 
284   g_return_if_fail (GST_IS_CURL_SSH_SINK (object));
285   sink = GST_CURL_SSH_SINK (object);
286 
287   switch (prop_id) {
288     case PROP_SSH_AUTH_TYPE:
289       g_value_set_enum (value, sink->ssh_auth_type);
290       break;
291 
292     case PROP_SSH_PUB_KEYFILE:
293       g_value_set_string (value, sink->ssh_pub_keyfile);
294       break;
295 
296     case PROP_SSH_PRIV_KEYFILE:
297       g_value_set_string (value, sink->ssh_priv_keyfile);
298       break;
299 
300     case PROP_SSH_KEY_PASSPHRASE:
301       g_value_set_string (value, sink->ssh_key_passphrase);
302       break;
303 
304     case PROP_SSH_KNOWNHOSTS:
305       g_value_set_string (value, sink->ssh_knownhosts);
306       break;
307 
308     case PROP_SSH_HOST_PUBLIC_KEY_MD5:
309       g_value_set_string (value, sink->ssh_host_public_key_md5);
310       break;
311 
312     case PROP_SSH_ACCEPT_UNKNOWNHOST:
313       g_value_set_boolean (value, sink->ssh_accept_unknownhost);
314       break;
315 
316     default:
317       GST_DEBUG_OBJECT (sink, "invalid property id");
318       break;
319   }
320 }
321 
322 static gboolean
gst_curl_ssh_sink_set_options_unlocked(GstCurlBaseSink * bcsink)323 gst_curl_ssh_sink_set_options_unlocked (GstCurlBaseSink * bcsink)
324 {
325   GstCurlSshSink *sink = GST_CURL_SSH_SINK (bcsink);
326   CURLcode curl_err = CURLE_OK;
327 
328   /* set SSH specific options here */
329   if (sink->ssh_pub_keyfile) {
330     if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_PUBLIC_KEYFILE,
331                 sink->ssh_pub_keyfile)) != CURLE_OK) {
332       bcsink->error = g_strdup_printf ("failed to set public key file: %s",
333           curl_easy_strerror (curl_err));
334       return FALSE;
335     }
336   }
337 
338   if (sink->ssh_priv_keyfile) {
339     if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_PRIVATE_KEYFILE,
340                 sink->ssh_priv_keyfile)) != CURLE_OK) {
341       bcsink->error = g_strdup_printf ("failed to set private key file: %s",
342           curl_easy_strerror (curl_err));
343       return FALSE;
344     }
345   }
346 
347   if (sink->ssh_knownhosts) {
348     if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KNOWNHOSTS,
349                 sink->ssh_knownhosts)) != CURLE_OK) {
350       bcsink->error = g_strdup_printf ("failed to set known_hosts file: %s",
351           curl_easy_strerror (curl_err));
352       return FALSE;
353     }
354   }
355 
356   if (sink->ssh_host_public_key_md5) {
357     /* libcurl is freaking tricky. If the input string is not exactly 32
358      * hexdigits long it silently ignores CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 and
359      * performs the transfer without authenticating the server! */
360     if (strlen (sink->ssh_host_public_key_md5) != 32) {
361       bcsink->error = g_strdup ("MD5-hash string has invalid length, "
362           "must be exactly 32 hexdigits!");
363       return FALSE;
364     }
365 
366     if ((curl_err =
367             curl_easy_setopt (bcsink->curl, CURLOPT_SSH_HOST_PUBLIC_KEY_MD5,
368                 sink->ssh_host_public_key_md5)) != CURLE_OK) {
369       bcsink->error = g_strdup_printf ("failed to set remote host's public "
370           "key MD5: %s", curl_easy_strerror (curl_err));
371       return FALSE;
372     }
373   }
374 
375   /* make sure we only accept PASSWORD or PUBLICKEY auth methods
376    * (can be extended later) */
377   if (sink->ssh_auth_type == GST_CURLSSH_AUTH_PASSWORD ||
378       sink->ssh_auth_type == GST_CURLSSH_AUTH_PUBLICKEY) {
379 
380     /* set the SSH_AUTH_TYPE */
381     if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_AUTH_TYPES,
382                 sink->ssh_auth_type)) != CURLE_OK) {
383       bcsink->error = g_strdup_printf ("failed to set authentication type: %s",
384           curl_easy_strerror (curl_err));
385       return FALSE;
386     }
387 
388     /* if key authentication -> provide the private key passphrase as well */
389     if (sink->ssh_auth_type == GST_CURLSSH_AUTH_PUBLICKEY) {
390       if (sink->ssh_key_passphrase) {
391         if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_KEYPASSWD,
392                     sink->ssh_key_passphrase)) != CURLE_OK) {
393           bcsink->error = g_strdup_printf ("failed to set private key "
394               "passphrase: %s", curl_easy_strerror (curl_err));
395           return FALSE;
396         }
397       } else {
398         /* The user did not provide the passphrase for the private key.
399          * This can still be a valid situation, if (s)he chose not to
400          * protect the private key with a passphrase - but not recommended! */
401         GST_WARNING_OBJECT (sink, "Warning: key authentication chosen but "
402             "'ssh-key-passphrase' not provided!");
403       }
404     }
405 
406   } else {
407     bcsink->error = g_strdup_printf ("Error: unsupported authentication type: "
408         "%d.", sink->ssh_auth_type);
409     return FALSE;
410   }
411 
412   /* Install the SSH_KEYFUNCTION callback...
413    * IMPORTANT: this callback gets called only if CURLOPT_SSH_KNOWNHOSTS
414    * is also set! */
415   if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KEYFUNCTION,
416               curl_ssh_sink_sshkey_cb)) != CURLE_OK) {
417     bcsink->error = g_strdup_printf ("failed to set SSH_KEYFUNCTION callback: "
418         "%s", curl_easy_strerror (curl_err));
419     return FALSE;
420   }
421   /* SSH_KEYFUNCTION callback successfully installed so go on and
422    * set the '*clientp' parameter as well */
423   if ((curl_err =
424           curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KEYDATA,
425               sink)) != CURLE_OK) {
426     bcsink->error = g_strdup_printf ("failed to set CURLOPT_SSH_KEYDATA: %s",
427         curl_easy_strerror (curl_err));
428     return FALSE;
429   }
430 
431   return TRUE;
432 }
433 
434 
435 /* A 'curl_sshkey_cb' callback function. It gets called when the known_host
436  * matching has been done, to allow the application to act and decide for
437  * libcurl how to proceed.
438  * The callback will only be called if CURLOPT_SSH_KNOWNHOSTS is also set!
439  * NOTE:
440  *  * use CURLOPT_SSH_KEYFUNCTION to install the callback func
441  *  * use CURLOPT_SSH_KEYDATA to pass in the actual "*clientp"
442  */
443 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)444 curl_ssh_sink_sshkey_cb (CURL * easy_handle,    /* easy handle */
445     const struct curl_khkey *knownkey,  /* known - key from known_hosts */
446     const struct curl_khkey *foundkey,  /* found - key from remote end */
447     enum curl_khmatch match,    /* libcurl's view on the keys */
448     void *clientp)
449 {
450   GstCurlSshSink *sink = (GstCurlSshSink *) clientp;
451 
452   /* the default action to be taken upon pub key matching */
453   guint res_action = CURLKHSTAT_REJECT;
454 
455   switch (match) {
456     case CURLKHMATCH_OK:
457       res_action = CURLKHSTAT_FINE;
458       GST_INFO_OBJECT (sink,
459           "Remote public host key is matching known_hosts, OK to proceed.");
460       break;
461 
462     case CURLKHMATCH_MISMATCH:
463       GST_WARNING_OBJECT (sink,
464           "Remote public host key mismatch in known_hosts, aborting "
465           "connection.");
466       /* Reject the connection. The old mismatching key has to be manually
467        * removed from 'known_hosts' before being able to connect again to
468        * the respective host. */
469       break;
470 
471     case CURLKHMATCH_MISSING:
472       GST_OBJECT_LOCK (sink);
473       if (sink->ssh_accept_unknownhost == TRUE) {
474         /* the key was not found in known_hosts but the user chose to
475          * accept it */
476         res_action = CURLKHSTAT_FINE_ADD_TO_FILE;
477         GST_INFO_OBJECT (sink, "Accepting and adding new public host key to "
478             "known_hosts.");
479       } else {
480         /* the key was not found in known_hosts and the user chose not
481          * to accept connections to unknown hosts */
482         GST_WARNING_OBJECT (sink,
483             "Remote public host key is unknown, rejecting connection.");
484       }
485       GST_OBJECT_UNLOCK (sink);
486       break;
487 
488     default:
489       /* something went wrong, we got some bogus key match result */
490       GST_CURL_BASE_SINK (sink)->error =
491           g_strdup ("libcurl internal error during known_host matching");
492       break;
493   }
494 
495   return res_action;
496 }
497