/* GStreamer * Copyright (C) 2011 Axis Communications * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301, USA. */ /** * SECTION:element-curlsshsink * @title: curlsshsink * @short_description: sink that uploads data to a server using libcurl * * This is a network sink that uses libcurl. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "gstcurlbasesink.h" #include "gstcurlsshsink.h" #include #include #include #ifdef G_OS_WIN32 #include #else #include #include #include #include #endif #include #include #include #include /* Default values */ #define GST_CAT_DEFAULT gst_curl_ssh_sink_debug /* Plugin specific settings */ GST_DEBUG_CATEGORY_STATIC (gst_curl_ssh_sink_debug); enum { PROP_0, PROP_SSH_AUTH_TYPE, PROP_SSH_PUB_KEYFILE, PROP_SSH_PRIV_KEYFILE, PROP_SSH_KEY_PASSPHRASE, PROP_SSH_KNOWNHOSTS, PROP_SSH_HOST_PUBLIC_KEY_MD5, PROP_SSH_ACCEPT_UNKNOWNHOST }; /* curl SSH-key matching callback */ static gint curl_ssh_sink_sshkey_cb (CURL * easy_handle, const struct curl_khkey *knownkey, const struct curl_khkey *foundkey, enum curl_khmatch, void *clientp); /* Object class function declarations */ static void gst_curl_ssh_sink_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_curl_ssh_sink_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static void gst_curl_ssh_sink_finalize (GObject * gobject); static gboolean gst_curl_ssh_sink_set_options_unlocked (GstCurlBaseSink * bcsink); /* private functions */ #define gst_curl_ssh_sink_parent_class parent_class G_DEFINE_TYPE (GstCurlSshSink, gst_curl_ssh_sink, GST_TYPE_CURL_BASE_SINK); /* Register the auth types with the GLib type system */ #define GST_TYPE_CURL_SSH_SINK_AUTH_TYPE (gst_curl_ssh_sink_auth_get_type ()) static GType gst_curl_ssh_sink_auth_get_type (void) { static GType gtype = 0; if (!gtype) { static const GEnumValue auth_types[] = { {GST_CURLSSH_AUTH_NONE, "Not allowed", "none"}, {GST_CURLSSH_AUTH_PUBLICKEY, "Public/private key files", "pubkey"}, {GST_CURLSSH_AUTH_PASSWORD, "Password authentication", "password"}, {0, NULL, NULL} }; gtype = g_enum_register_static ("GstCurlSshAuthType", auth_types); } return gtype; } static void gst_curl_ssh_sink_class_init (GstCurlSshSinkClass * klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GstElementClass *element_class = GST_ELEMENT_CLASS (klass); GST_DEBUG_CATEGORY_INIT (gst_curl_ssh_sink_debug, "curlsshsink", 0, "curl ssh sink element"); GST_DEBUG_OBJECT (klass, "class_init"); gst_element_class_set_static_metadata (element_class, "Curl SSH sink", "Sink/Network", "Upload data over SSH/SFTP using libcurl", "Sorin L. "); gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_curl_ssh_sink_finalize); gobject_class->set_property = gst_curl_ssh_sink_set_property; gobject_class->get_property = gst_curl_ssh_sink_get_property; klass->set_options_unlocked = gst_curl_ssh_sink_set_options_unlocked; g_object_class_install_property (gobject_class, PROP_SSH_AUTH_TYPE, g_param_spec_enum ("ssh-auth-type", "SSH authentication type", "SSH authentication method to authenticate on the SSH/SFTP server", GST_TYPE_CURL_SSH_SINK_AUTH_TYPE, GST_CURLSSH_AUTH_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SSH_PUB_KEYFILE, g_param_spec_string ("ssh-pub-keyfile", "SSH public key file", "The complete path & filename of the SSH public key file", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SSH_PRIV_KEYFILE, g_param_spec_string ("ssh-priv-keyfile", "SSH private key file", "The complete path & filename of the SSH private key file", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SSH_KEY_PASSPHRASE, g_param_spec_string ("ssh-key-passphrase", "Passphrase of the priv key", "The passphrase used to protect the SSH private key file", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SSH_KNOWNHOSTS, g_param_spec_string ("ssh-knownhosts", "SSH known hosts", "The complete path & filename of the SSH 'known_hosts' file", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SSH_HOST_PUBLIC_KEY_MD5, g_param_spec_string ("ssh-host-pubkey-md5", "MD5 checksum of the remote host's public key", "MD5 checksum (32 hexadecimal digits, case-insensitive) of the " "remote host's public key", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SSH_ACCEPT_UNKNOWNHOST, g_param_spec_boolean ("ssh-accept-unknownhost", "SSH accept unknown host", "Accept an unknown remote public host key", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); gst_type_mark_as_plugin_api (GST_TYPE_CURL_SSH_SINK_AUTH_TYPE, 0); } static void gst_curl_ssh_sink_init (GstCurlSshSink * sink) { sink->ssh_auth_type = GST_CURLSSH_AUTH_NONE; sink->ssh_pub_keyfile = NULL; sink->ssh_priv_keyfile = NULL; sink->ssh_key_passphrase = NULL; sink->ssh_knownhosts = NULL; sink->ssh_host_public_key_md5 = NULL; sink->ssh_accept_unknownhost = FALSE; } static void gst_curl_ssh_sink_finalize (GObject * gobject) { GstCurlSshSink *this = GST_CURL_SSH_SINK (gobject); GST_DEBUG ("finalizing curlsshsink"); g_free (this->ssh_pub_keyfile); g_free (this->ssh_priv_keyfile); g_free (this->ssh_key_passphrase); g_free (this->ssh_knownhosts); g_free (this->ssh_host_public_key_md5); G_OBJECT_CLASS (parent_class)->finalize (gobject); } static void gst_curl_ssh_sink_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstCurlSshSink *sink; GstState cur_state; g_return_if_fail (GST_IS_CURL_SSH_SINK (object)); sink = GST_CURL_SSH_SINK (object); gst_element_get_state (GST_ELEMENT (sink), &cur_state, NULL, 0); if (cur_state == GST_STATE_PLAYING || cur_state == GST_STATE_PAUSED) { return; } GST_OBJECT_LOCK (sink); switch (prop_id) { case PROP_SSH_AUTH_TYPE: sink->ssh_auth_type = g_value_get_enum (value); GST_DEBUG_OBJECT (sink, "ssh_auth_type set to %d", sink->ssh_auth_type); break; case PROP_SSH_PUB_KEYFILE: g_free (sink->ssh_pub_keyfile); sink->ssh_pub_keyfile = g_value_dup_string (value); GST_DEBUG_OBJECT (sink, "ssh_pub_keyfile set to %s", sink->ssh_pub_keyfile); break; case PROP_SSH_PRIV_KEYFILE: g_free (sink->ssh_priv_keyfile); sink->ssh_priv_keyfile = g_value_dup_string (value); GST_DEBUG_OBJECT (sink, "ssh_priv_keyfile set to %s", sink->ssh_priv_keyfile); break; case PROP_SSH_KEY_PASSPHRASE: g_free (sink->ssh_key_passphrase); sink->ssh_key_passphrase = g_value_dup_string (value); GST_DEBUG_OBJECT (sink, "ssh_key_passphrase set to %s", sink->ssh_key_passphrase); break; case PROP_SSH_KNOWNHOSTS: g_free (sink->ssh_knownhosts); sink->ssh_knownhosts = g_value_dup_string (value); GST_DEBUG_OBJECT (sink, "ssh_knownhosts set to %s", sink->ssh_knownhosts); break; case PROP_SSH_HOST_PUBLIC_KEY_MD5: g_free (sink->ssh_host_public_key_md5); sink->ssh_host_public_key_md5 = g_value_dup_string (value); GST_DEBUG_OBJECT (sink, "ssh_host_public_key_md5 set to %s", sink->ssh_host_public_key_md5); break; case PROP_SSH_ACCEPT_UNKNOWNHOST: sink->ssh_accept_unknownhost = g_value_get_boolean (value); GST_DEBUG_OBJECT (sink, "ssh_accept_unknownhost set to %d", sink->ssh_accept_unknownhost); break; default: GST_DEBUG_OBJECT (sink, "invalid property id %d", prop_id); break; } GST_OBJECT_UNLOCK (sink); } static void gst_curl_ssh_sink_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstCurlSshSink *sink; g_return_if_fail (GST_IS_CURL_SSH_SINK (object)); sink = GST_CURL_SSH_SINK (object); switch (prop_id) { case PROP_SSH_AUTH_TYPE: g_value_set_enum (value, sink->ssh_auth_type); break; case PROP_SSH_PUB_KEYFILE: g_value_set_string (value, sink->ssh_pub_keyfile); break; case PROP_SSH_PRIV_KEYFILE: g_value_set_string (value, sink->ssh_priv_keyfile); break; case PROP_SSH_KEY_PASSPHRASE: g_value_set_string (value, sink->ssh_key_passphrase); break; case PROP_SSH_KNOWNHOSTS: g_value_set_string (value, sink->ssh_knownhosts); break; case PROP_SSH_HOST_PUBLIC_KEY_MD5: g_value_set_string (value, sink->ssh_host_public_key_md5); break; case PROP_SSH_ACCEPT_UNKNOWNHOST: g_value_set_boolean (value, sink->ssh_accept_unknownhost); break; default: GST_DEBUG_OBJECT (sink, "invalid property id"); break; } } static gboolean gst_curl_ssh_sink_set_options_unlocked (GstCurlBaseSink * bcsink) { GstCurlSshSink *sink = GST_CURL_SSH_SINK (bcsink); CURLcode curl_err = CURLE_OK; /* set SSH specific options here */ if (sink->ssh_pub_keyfile) { if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_PUBLIC_KEYFILE, sink->ssh_pub_keyfile)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set public key file: %s", curl_easy_strerror (curl_err)); return FALSE; } } if (sink->ssh_priv_keyfile) { if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_PRIVATE_KEYFILE, sink->ssh_priv_keyfile)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set private key file: %s", curl_easy_strerror (curl_err)); return FALSE; } } if (sink->ssh_knownhosts) { if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KNOWNHOSTS, sink->ssh_knownhosts)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set known_hosts file: %s", curl_easy_strerror (curl_err)); return FALSE; } } if (sink->ssh_host_public_key_md5) { /* libcurl is freaking tricky. If the input string is not exactly 32 * hexdigits long it silently ignores CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 and * performs the transfer without authenticating the server! */ if (strlen (sink->ssh_host_public_key_md5) != 32) { bcsink->error = g_strdup ("MD5-hash string has invalid length, " "must be exactly 32 hexdigits!"); return FALSE; } if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, sink->ssh_host_public_key_md5)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set remote host's public " "key MD5: %s", curl_easy_strerror (curl_err)); return FALSE; } } /* make sure we only accept PASSWORD or PUBLICKEY auth methods * (can be extended later) */ if (sink->ssh_auth_type == GST_CURLSSH_AUTH_PASSWORD || sink->ssh_auth_type == GST_CURLSSH_AUTH_PUBLICKEY) { /* set the SSH_AUTH_TYPE */ if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_AUTH_TYPES, sink->ssh_auth_type)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set authentication type: %s", curl_easy_strerror (curl_err)); return FALSE; } /* if key authentication -> provide the private key passphrase as well */ if (sink->ssh_auth_type == GST_CURLSSH_AUTH_PUBLICKEY) { if (sink->ssh_key_passphrase) { if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_KEYPASSWD, sink->ssh_key_passphrase)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set private key " "passphrase: %s", curl_easy_strerror (curl_err)); return FALSE; } } else { /* The user did not provide the passphrase for the private key. * This can still be a valid situation, if (s)he chose not to * protect the private key with a passphrase - but not recommended! */ GST_WARNING_OBJECT (sink, "Warning: key authentication chosen but " "'ssh-key-passphrase' not provided!"); } } } else { bcsink->error = g_strdup_printf ("Error: unsupported authentication type: " "%d.", sink->ssh_auth_type); return FALSE; } /* Install the SSH_KEYFUNCTION callback... * IMPORTANT: this callback gets called only if CURLOPT_SSH_KNOWNHOSTS * is also set! */ if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KEYFUNCTION, curl_ssh_sink_sshkey_cb)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set SSH_KEYFUNCTION callback: " "%s", curl_easy_strerror (curl_err)); return FALSE; } /* SSH_KEYFUNCTION callback successfully installed so go on and * set the '*clientp' parameter as well */ if ((curl_err = curl_easy_setopt (bcsink->curl, CURLOPT_SSH_KEYDATA, sink)) != CURLE_OK) { bcsink->error = g_strdup_printf ("failed to set CURLOPT_SSH_KEYDATA: %s", curl_easy_strerror (curl_err)); return FALSE; } return TRUE; } /* A 'curl_sshkey_cb' callback function. It gets called when the known_host * matching has been done, to allow the application to act and decide for * libcurl how to proceed. * The callback will only be called if CURLOPT_SSH_KNOWNHOSTS is also set! * NOTE: * * use CURLOPT_SSH_KEYFUNCTION to install the callback func * * use CURLOPT_SSH_KEYDATA to pass in the actual "*clientp" */ static gint curl_ssh_sink_sshkey_cb (CURL * easy_handle, /* easy handle */ const struct curl_khkey *knownkey, /* known - key from known_hosts */ const struct curl_khkey *foundkey, /* found - key from remote end */ enum curl_khmatch match, /* libcurl's view on the keys */ void *clientp) { GstCurlSshSink *sink = (GstCurlSshSink *) clientp; /* the default action to be taken upon pub key matching */ guint res_action = CURLKHSTAT_REJECT; switch (match) { case CURLKHMATCH_OK: res_action = CURLKHSTAT_FINE; GST_INFO_OBJECT (sink, "Remote public host key is matching known_hosts, OK to proceed."); break; case CURLKHMATCH_MISMATCH: GST_WARNING_OBJECT (sink, "Remote public host key mismatch in known_hosts, aborting " "connection."); /* Reject the connection. The old mismatching key has to be manually * removed from 'known_hosts' before being able to connect again to * the respective host. */ break; case CURLKHMATCH_MISSING: GST_OBJECT_LOCK (sink); if (sink->ssh_accept_unknownhost == TRUE) { /* the key was not found in known_hosts but the user chose to * accept it */ res_action = CURLKHSTAT_FINE_ADD_TO_FILE; GST_INFO_OBJECT (sink, "Accepting and adding new public host key to " "known_hosts."); } else { /* the key was not found in known_hosts and the user chose not * to accept connections to unknown hosts */ GST_WARNING_OBJECT (sink, "Remote public host key is unknown, rejecting connection."); } GST_OBJECT_UNLOCK (sink); break; default: /* something went wrong, we got some bogus key match result */ GST_CURL_BASE_SINK (sink)->error = g_strdup ("libcurl internal error during known_host matching"); break; } return res_action; }