--- /dev/null
+/*
+ * virnetsshsession.c: ssh network transport provider based on libssh2
+ *
+ * Copyright (C) 2012 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Author: Peter Krempa <pkrempa@redhat.com>
+ */
+#include <config.h>
+#include <libssh2.h>
+#include <libssh2_publickey.h>
+
+#include "virnetsshsession.h"
+
+#include "internal.h"
+#include "buf.h"
+#include "memory.h"
+#include "logging.h"
+#include "configmake.h"
+#include "threads.h"
+#include "util.h"
+#include "virterror_internal.h"
+#include "virobject.h"
+
+#define VIR_FROM_THIS VIR_FROM_SSH
+
+static const char
+vir_libssh2_key_comment[] = "added by libvirt ssh transport";
+#define VIR_NET_SSH_BUFFER_SIZE 1024
+
+typedef enum {
+ VIR_NET_SSH_STATE_NEW,
+ VIR_NET_SSH_STATE_HANDSHAKE_COMPLETE,
+ VIR_NET_SSH_STATE_AUTH_CALLBACK_ERROR,
+ VIR_NET_SSH_STATE_CLOSED,
+ VIR_NET_SSH_STATE_ERROR,
+ VIR_NET_SSH_STATE_ERROR_REMOTE,
+} virNetSSHSessionState;
+
+typedef enum {
+ VIR_NET_SSH_AUTHCB_OK,
+ VIR_NET_SSH_AUTHCB_NO_METHOD,
+ VIR_NET_SSH_AUTHCB_OOM,
+ VIR_NET_SSH_AUTHCB_RETR_ERR,
+} virNetSSHAuthCallbackError;
+
+typedef enum {
+ VIR_NET_SSH_AUTH_KEYBOARD_INTERACTIVE,
+ VIR_NET_SSH_AUTH_PASSWORD,
+ VIR_NET_SSH_AUTH_PRIVKEY,
+ VIR_NET_SSH_AUTH_AGENT
+} virNetSSHAuthMethods;
+
+
+typedef struct _virNetSSHAuthMethod virNetSSHAuthMethod;
+typedef virNetSSHAuthMethod *virNetSSHAuthMethodPtr;
+
+struct _virNetSSHAuthMethod {
+ virNetSSHAuthMethods method;
+ char *username;
+ char *password;
+ char *filename;
+
+ int tries;
+};
+
+struct _virNetSSHSession {
+ virObject object;
+ virNetSSHSessionState state;
+ virMutex lock;
+
+ /* libssh2 internal stuff */
+ LIBSSH2_SESSION *session;
+ LIBSSH2_CHANNEL *channel;
+ LIBSSH2_KNOWNHOSTS *knownHosts;
+ LIBSSH2_AGENT *agent;
+
+ /* for host key checking */
+ virNetSSHHostkeyVerify hostKeyVerify;
+ char *knownHostsFile;
+ char *hostname;
+ int port;
+
+ /* authentication stuff */
+ virConnectAuthPtr cred;
+ virNetSSHAuthCallbackError authCbErr;
+ size_t nauths;
+ virNetSSHAuthMethodPtr *auths;
+
+ /* channel stuff */
+ char *channelCommand;
+ int channelCommandReturnValue;
+
+ /* read cache */
+ char rbuf[VIR_NET_SSH_BUFFER_SIZE];
+ size_t bufUsed;
+ size_t bufStart;
+};
+
+static void
+virNetSSHSessionAuthMethodsFree(virNetSSHSessionPtr sess)
+{
+ int i;
+
+ for (i = 0; i < sess->nauths; i++) {
+ VIR_FREE(sess->auths[i]->username);
+ VIR_FREE(sess->auths[i]->password);
+ VIR_FREE(sess->auths[i]->filename);
+ VIR_FREE(sess->auths[i]);
+ }
+
+ VIR_FREE(sess->auths);
+ sess->nauths = 0;
+}
+
+static void
+virNetSSHSessionDispose(void *obj)
+{
+ virNetSSHSessionPtr sess = obj;
+ VIR_DEBUG("sess=0x%p", sess);
+
+ if (!sess)
+ return;
+
+ if (sess->channel) {
+ libssh2_channel_send_eof(sess->channel);
+ libssh2_channel_close(sess->channel);
+ libssh2_channel_free(sess->channel);
+ }
+
+ libssh2_knownhost_free(sess->knownHosts);
+ libssh2_agent_free(sess->agent);
+
+ if (sess->session) {
+ libssh2_session_disconnect(sess->session,
+ "libvirt: virNetSSHSessionFree()");
+ libssh2_session_free(sess->session);
+ }
+
+ virNetSSHSessionAuthMethodsFree(sess);
+
+ VIR_FREE(sess->channelCommand);
+ VIR_FREE(sess->hostname);
+ VIR_FREE(sess->knownHostsFile);
+}
+
+static virClassPtr virNetSSHSessionClass;
+static int
+virNetSSHSessionOnceInit(void)
+{
+ if (!(virNetSSHSessionClass = virClassNew("virNetSSHSession",
+ sizeof(virNetSSHSession),
+ virNetSSHSessionDispose)))
+ return -1;
+
+ return 0;
+}
+VIR_ONCE_GLOBAL_INIT(virNetSSHSession);
+
+static virNetSSHAuthMethodPtr
+virNetSSHSessionAuthMethodNew(virNetSSHSessionPtr sess)
+{
+ virNetSSHAuthMethodPtr auth;
+
+ if (VIR_ALLOC(auth) < 0)
+ goto error;
+
+ if (VIR_EXPAND_N(sess->auths, sess->nauths, 1) < 0)
+ goto error;
+
+ sess->auths[sess->nauths - 1] = auth;
+
+ return auth;
+
+error:
+ VIR_FREE(auth);
+ return NULL;
+}
+
+/* keyboard interactive authentication callback */
+static void
+virNetSSHKbIntCb(const char *name ATTRIBUTE_UNUSED,
+ int name_len ATTRIBUTE_UNUSED,
+ const char *instruction ATTRIBUTE_UNUSED,
+ int instruction_len ATTRIBUTE_UNUSED,
+ int num_prompts,
+ const LIBSSH2_USERAUTH_KBDINT_PROMPT *prompts,
+ LIBSSH2_USERAUTH_KBDINT_RESPONSE *responses,
+ void **opaque)
+{
+ virNetSSHSessionPtr priv = *opaque;
+ virConnectCredentialPtr askcred = NULL;
+ int i;
+ int credtype_echo = -1;
+ int credtype_noecho = -1;
+ char *tmp;
+
+ priv->authCbErr = VIR_NET_SSH_AUTHCB_OK;
+
+ /* find credential type for asking passwords */
+ for (i = 0; i < priv->cred->ncredtype; i++) {
+ if (priv->cred->credtype[i] == VIR_CRED_PASSPHRASE ||
+ priv->cred->credtype[i] == VIR_CRED_NOECHOPROMPT)
+ credtype_noecho = priv->cred->credtype[i];
+
+ if (priv->cred->credtype[i] == VIR_CRED_ECHOPROMPT)
+ credtype_echo = priv->cred->credtype[i];
+ }
+
+ if (credtype_echo < 0 || credtype_noecho < 0) {
+ priv->authCbErr = VIR_NET_SSH_AUTHCB_NO_METHOD;
+ return;
+ }
+
+ if (VIR_ALLOC_N(askcred, num_prompts) < 0) {
+ priv->authCbErr = VIR_NET_SSH_AUTHCB_OOM;
+ return;
+ }
+
+ /* fill data structures for auth callback */
+ for (i = 0; i < num_prompts; i++) {
+ if (!(askcred[i].prompt = strdup(prompts[i].text))) {
+ priv->authCbErr = VIR_NET_SSH_AUTHCB_OOM;
+ goto cleanup;
+ }
+
+ /* remove colon and trailing spaces from prompts, as default behavior
+ * of libvirt's auth callback is to add them */
+ if ((tmp = strrchr(askcred[i].prompt, ':')))
+ *tmp = '\0';
+
+ askcred[i].type = prompts[i].echo ? credtype_echo : credtype_noecho;
+ }
+
+ /* retrieve responses using the auth callback */
+ if (priv->cred->cb(askcred, num_prompts, priv->cred->cbdata)) {
+ priv->authCbErr = VIR_NET_SSH_AUTHCB_RETR_ERR;
+ goto cleanup;
+ }
+
+ /* copy retrieved data back */
+ for (i = 0; i < num_prompts; i++) {
+ responses[i].text = askcred[i].result;
+ askcred[i].result = NULL; /* steal the pointer */
+ responses[i].length = askcred[i].resultlen;
+ }
+
+cleanup:
+ if (askcred) {
+ for (i = 0; i < num_prompts; i++) {
+ VIR_FREE(askcred[i].result);
+ VIR_FREE(askcred[i].prompt);
+ }
+ }
+
+ VIR_FREE(askcred);
+
+ return;
+}
+
+/* check session host keys
+ *
+ * this function checks the known host database and verifies the key
+ * errors are raised in this func
+ *
+ * return value: 0 on success, -1 on error
+ */
+static int
+virNetSSHCheckHostKey(virNetSSHSessionPtr sess)
+{
+ int ret;
+ const char *key;
+ const char *keyhash;
+ int keyType;
+ size_t keyLength;
+ char *errmsg;
+ virBuffer buff = VIR_BUFFER_INITIALIZER;
+ virConnectCredential askKey;
+ struct libssh2_knownhost *knownHostEntry = NULL;
+ int i;
+ char *hostnameStr = NULL;
+
+ if (sess->hostKeyVerify == VIR_NET_SSH_HOSTKEY_VERIFY_IGNORE)
+ return 0;
+
+ /* get the key */
+ key = libssh2_session_hostkey(sess->session, &keyLength, &keyType);
+ if (!key) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("Failed to retrieve ssh host key: %s"),
+ errmsg);
+ return -1;
+ }
+
+ /* verify it */
+ ret = libssh2_knownhost_checkp(sess->knownHosts,
+ sess->hostname,
+ sess->port,
+ key,
+ keyLength,
+ LIBSSH2_KNOWNHOST_TYPE_PLAIN |
+ LIBSSH2_KNOWNHOST_KEYENC_RAW,
+ &knownHostEntry);
+
+ switch (ret) {
+ case LIBSSH2_KNOWNHOST_CHECK_NOTFOUND:
+ /* key was not found, query to add it to database */
+ if (sess->hostKeyVerify == VIR_NET_SSH_HOSTKEY_VERIFY_NORMAL) {
+ /* ask to add the key */
+ if (!sess->cred || !sess->cred->cb) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("No user interaction callback provided: "
+ "Can't verify the session host key"));
+ return -1;
+ }
+
+ /* prepare data for the callback */
+ memset(&askKey, 0, sizeof(virConnectCredential));
+
+ for (i = 0; i < sess->cred->ncredtype; i++) {
+ if (sess->cred->credtype[i] == VIR_CRED_ECHOPROMPT) {
+ i = -1;
+ break;
+ }
+ }
+
+ if (i > 0) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("no suitable method to retrieve "
+ "authentication cretentials"));
+ return -1;
+ }
+
+ /* calculate remote key hash, using MD5 algorithm that is
+ * usual in OpenSSH. The returned value should *NOT* be freed*/
+ if (!(keyhash = libssh2_hostkey_hash(sess->session,
+ LIBSSH2_HOSTKEY_HASH_MD5))) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("failed to calculate ssh host key hash"));
+ return -1;
+ }
+ /* format the host key into a nice userfriendly string.
+ * Sadly, there's no constant to describe the hash length, so
+ * we have to use a *MAGIC* constant. */
+ for (i = 0; i < 16; i++)
+ virBufferAsprintf(&buff, "%02hhX:", keyhash[i]);
+ virBufferTrim(&buff, ":", 1);
+
+ if (virBufferError(&buff) != 0) {
+ virReportOOMError();
+ return -1;
+ }
+
+ keyhash = virBufferContentAndReset(&buff);
+
+ askKey.type = VIR_CRED_ECHOPROMPT;
+ if (virAsprintf((char **)&askKey.prompt,
+ _("Accept SSH host key with hash '%s' for "
+ "host '%s:%d' (%s/%s)?"),
+ keyhash,
+ sess->hostname, sess->port,
+ "y", "n") < 0) {
+ virReportOOMError();
+ VIR_FREE(keyhash);
+ return -1;
+ }
+
+ if (sess->cred->cb(&askKey, 1, sess->cred->cbdata)) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("failed to retrieve decision to accept "
+ "host key"));
+ VIR_FREE(askKey.prompt);
+ VIR_FREE(keyhash);
+ return -1;
+ }
+
+ VIR_FREE(askKey.prompt);
+
+ if (!askKey.result ||
+ STRCASENEQ(askKey.result, "y")) {
+ virReportError(VIR_ERR_SSH,
+ _("SSH host key for '%s' (%s) was not accepted"),
+ sess->hostname, keyhash);
+ VIR_FREE(keyhash);
+ VIR_FREE(askKey.result);
+ return -1;
+ }
+ VIR_FREE(keyhash);
+ VIR_FREE(askKey.result);
+ }
+
+ /* VIR_NET_SSH_HOSTKEY_VERIFY_AUTO_ADD */
+ /* convert key type, as libssh is using different enums type for
+ * getting the key and different for adding ... */
+ switch (keyType) {
+ case LIBSSH2_HOSTKEY_TYPE_RSA:
+ keyType = LIBSSH2_KNOWNHOST_KEY_SSHRSA;
+ break;
+ case LIBSSH2_HOSTKEY_TYPE_DSS:
+ keyType = LIBSSH2_KNOWNHOST_KEY_SSHDSS;
+
+ case LIBSSH2_HOSTKEY_TYPE_UNKNOWN:
+ default:
+ virReportError(VIR_ERR_SSH, "%s",
+ _("unsupported SSH key type"));
+ return -1;
+ }
+
+ /* add the key to the DB and save it, if applicable */
+ /* construct a "[hostname]:port" string to have the hostkey bound
+ * to port number */
+ virBufferAsprintf(&buff, "[%s]:%d", sess->hostname, sess->port);
+
+ if (virBufferError(&buff) != 0) {
+ virReportOOMError();
+ return -1;
+ }
+
+ hostnameStr = virBufferContentAndReset(&buff);
+
+ if (libssh2_knownhost_addc(sess->knownHosts,
+ hostnameStr,
+ NULL,
+ key,
+ keyLength,
+ vir_libssh2_key_comment,
+ strlen(vir_libssh2_key_comment),
+ LIBSSH2_KNOWNHOST_TYPE_PLAIN |
+ LIBSSH2_KNOWNHOST_KEYENC_RAW |
+ keyType,
+ NULL) < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("unable to add SSH host key for host '%s': %s"),
+ hostnameStr, errmsg);
+ VIR_FREE(hostnameStr);
+ return -1;
+ }
+
+ VIR_FREE(hostnameStr);
+
+ /* write the host key file - if applicable */
+ if (sess->knownHostsFile) {
+ if (libssh2_knownhost_writefile(sess->knownHosts,
+ sess->knownHostsFile,
+ LIBSSH2_KNOWNHOST_FILE_OPENSSH) < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("failed to write known_host file '%s': %s"),
+ sess->knownHostsFile,
+ errmsg);
+ return -1;
+ }
+ }
+ /* key was accepted and added */
+ return 0;
+
+ case LIBSSH2_KNOWNHOST_CHECK_MATCH:
+ /* host key matches */
+ return 0;
+
+ case LIBSSH2_KNOWNHOST_CHECK_MISMATCH:
+ /* host key verification failed */
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("!!! SSH HOST KEY VERIFICATION FAILED !!!: "
+ "Identity of host '%s:%d' differs from stored identity. "
+ "Please verify the new host key '%s' to avoid possible "
+ "man in the middle attack. The key is stored in '%s'."),
+ sess->hostname, sess->port,
+ knownHostEntry->key, sess->knownHostsFile);
+ return -1;
+
+ case LIBSSH2_KNOWNHOST_CHECK_FAILURE:
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("failed to validate SSH host key: %s"),
+ errmsg);
+ return -1;
+
+ default: /* should never happen (tm) */
+ virReportError(VIR_ERR_SSH, "%s", _("Unknown error value"));
+ return -1;
+ }
+
+ return -1;
+}
+
+/* perform ssh agent authentication
+ *
+ * Returns: 0 on success
+ * 1 on authentication failure
+ * -1 on error
+ */
+static int
+virNetSSHAuthenticateAgent(virNetSSHSessionPtr sess,
+ virNetSSHAuthMethodPtr priv)
+{
+ struct libssh2_agent_publickey *agent_identity = NULL;
+ bool no_identity = true;
+ int ret;
+ char *errmsg;
+
+ if (libssh2_agent_connect(sess->agent) < 0) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Failed to connect to ssh agent"));
+ return 1;
+ }
+
+ if (libssh2_agent_list_identities(sess->agent) < 0) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Failed to list ssh agent identities"));
+ return 1;
+ }
+
+ while (!(ret = libssh2_agent_get_identity(sess->agent,
+ &agent_identity,
+ agent_identity))) {
+ no_identity = false;
+ if (!(ret = libssh2_agent_userauth(sess->agent,
+ priv->username,
+ agent_identity)))
+ return 0; /* key accepted */
+
+ if (ret != LIBSSH2_ERROR_AUTHENTICATION_FAILED &&
+ ret != LIBSSH2_ERROR_PUBLICKEY_UNRECOGNIZED &&
+ ret != LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("failed to authenticate using SSH agent: %s"),
+ errmsg);
+ return -1;
+ }
+ /* authentication has failed, try next key */
+ }
+
+ /* if there are no more keys in the agent, the identity retrieval
+ * function returns 1 */
+ if (ret == 1) {
+ if (no_identity) {
+ virReportError(VIR_ERR_AUTH_FAILED, "%s",
+ _("SSH Agent did not provide any "
+ "authentication identity"));
+ } else {
+ virReportError(VIR_ERR_AUTH_FAILED, "%s",
+ _("All identities provided by the SSH Agent "
+ "were rejected"));
+ }
+ return 1;
+ }
+
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("failed to authenticate using SSH agent: %s"),
+ errmsg);
+ return -1;
+}
+
+/* perform private key authentication
+ *
+ * Returns: 0 on success
+ * 1 on authentication failure
+ * -1 on error
+ */
+static int
+virNetSSHAuthenticatePrivkey(virNetSSHSessionPtr sess,
+ virNetSSHAuthMethodPtr priv)
+{
+ virConnectCredential retr_passphrase;
+ int i;
+ char *errmsg;
+ int ret;
+
+ /* try open the key with no password */
+ if ((ret = libssh2_userauth_publickey_fromfile(sess->session,
+ priv->username,
+ NULL,
+ priv->filename,
+ priv->password)) == 0)
+ return 0; /* success */
+
+ if (priv->password ||
+ ret == LIBSSH2_ERROR_PUBLICKEY_UNRECOGNIZED ||
+ ret == LIBSSH2_ERROR_AUTHENTICATION_FAILED) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("authentication with private key '%s' "
+ "has failed: %s"),
+ priv->filename, errmsg);
+ return 1; /* auth failed */
+ }
+
+ /* request user's key password */
+ if (!sess->cred || !sess->cred->cb) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("No user interaction callback provided: "
+ "Can't retrieve private key passphrase"));
+ return -1;
+ }
+
+ memset(&retr_passphrase, 0, sizeof(virConnectCredential));
+ retr_passphrase.type = -1;
+
+ for (i = 0; i < sess->cred->ncredtype; i++) {
+ if (sess->cred->credtype[i] == VIR_CRED_PASSPHRASE ||
+ sess->cred->credtype[i] == VIR_CRED_NOECHOPROMPT) {
+ retr_passphrase.type = sess->cred->credtype[i];
+ break;
+ }
+ }
+
+ if (retr_passphrase.type < 0) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("no suitable method to retrieve key passphrase"));
+ return -1;
+ }
+
+ if (virAsprintf((char **)&retr_passphrase.prompt,
+ _("Passphrase for key '%s'"),
+ priv->filename) < 0) {
+ virReportOOMError();
+ return -1;
+ }
+
+ if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("failed to retrieve private key passphrase: "
+ "callback has failed"));
+ VIR_FREE(retr_passphrase.prompt);
+ return -1;
+ }
+
+ VIR_FREE(retr_passphrase.prompt);
+
+ ret = libssh2_userauth_publickey_fromfile(sess->session,
+ priv->username,
+ NULL,
+ priv->filename,
+ retr_passphrase.result);
+
+ VIR_FREE(retr_passphrase.result);
+
+ if (ret < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("authentication with private key '%s' "
+ "has failed: %s"),
+ priv->filename, errmsg);
+
+ if (ret == LIBSSH2_ERROR_PUBLICKEY_UNRECOGNIZED ||
+ ret == LIBSSH2_ERROR_AUTHENTICATION_FAILED)
+ return 1;
+ else
+ return -1;
+ }
+
+ return 0;
+}
+
+/* perform tunelled password authentication
+ *
+ * Returns: 0 on success
+ * 1 on authentication failure
+ * -1 on error
+ */
+static int
+virNetSSHAuthenticatePassword(virNetSSHSessionPtr sess,
+ virNetSSHAuthMethodPtr priv)
+{
+ char *errmsg;
+ int ret;
+
+ /* tunelled password authentication */
+ if ((ret = libssh2_userauth_password(sess->session,
+ priv->username,
+ priv->password)) < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("tunelled password authentication failed: %s"),
+ errmsg);
+
+ if (ret == LIBSSH2_ERROR_AUTHENTICATION_FAILED)
+ return 1;
+ else
+ return -1;
+ }
+ /* auth success */
+ return 0;
+}
+
+/* perform keyboard interactive authentication
+ *
+ * Returns: 0 on success
+ * 1 on authentication failure
+ * -1 on error
+ */
+static int
+virNetSSHAuthenticateKeyboardInteractive(virNetSSHSessionPtr sess,
+ virNetSSHAuthMethodPtr priv)
+{
+ char *errmsg;
+ int ret;
+
+ if (!sess->cred || !sess->cred->cb) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Can't perform keyboard-interactive authentication: "
+ "Authentication callback not provided "));
+ return -1;
+ }
+
+ /* Try the authenticating the set amount of times. The server breaks the
+ * connection if maximum number of bad auth tries is exceeded */
+ while (priv->tries < 0 || priv->tries-- > 0) {
+ ret = libssh2_userauth_keyboard_interactive(sess->session,
+ priv->username,
+ virNetSSHKbIntCb);
+
+ /* check for errors while calling the callback */
+ switch (sess->authCbErr) {
+ case VIR_NET_SSH_AUTHCB_NO_METHOD:
+ virReportError(VIR_ERR_SSH, "%s",
+ _("no suitable method to retrieve "
+ "authentication cretentials"));
+ return -1;
+ case VIR_NET_SSH_AUTHCB_OOM:
+ virReportOOMError();
+ return -1;
+ case VIR_NET_SSH_AUTHCB_RETR_ERR:
+ virReportError(VIR_ERR_SSH, "%s",
+ _("failed to retrieve credentials"));
+ return -1;
+ case VIR_NET_SSH_AUTHCB_OK:
+ /* everything went fine, let's continue */
+ break;
+ }
+
+ if (ret == 0)
+ /* authentication succeeded */
+ return 0;
+
+ if (ret == LIBSSH2_ERROR_AUTHENTICATION_FAILED)
+ continue; /* try again */
+
+ if (ret < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("keyboard interactive authentication failed: %s"),
+ errmsg);
+ return -1;
+ }
+ }
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_AUTH_FAILED,
+ _("keyboard interactive authentication failed: %s"),
+ errmsg);
+ return 1;
+}
+
+/* select auth method and authenticate */
+static int
+virNetSSHAuthenticate(virNetSSHSessionPtr sess)
+{
+ virNetSSHAuthMethodPtr auth;
+ bool no_method = false;
+ bool auth_failed = false;
+ char *auth_list;
+ char *errmsg;
+ int i;
+ int ret;
+
+ if (!sess->nauths) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("No authentication methods and credentials "
+ "provided"));
+ return -1;
+ }
+
+ /* obtain list of supported auth methods */
+ auth_list = libssh2_userauth_list(sess->session,
+ sess->auths[0]->username,
+ strlen(sess->auths[0]->username));
+ if (!auth_list) {
+ /* unlikely event, authentication succeeded with NONE as method */
+ if (libssh2_userauth_authenticated(sess->session) == 1)
+ return 0;
+
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("couldn't retrieve authentication methods list: %s"),
+ errmsg);
+ return -1;
+ }
+
+ for (i = 0; i < sess->nauths; i++) {
+ auth = sess->auths[i];
+
+ ret = 2;
+ virResetLastError();
+
+ switch (auth->method) {
+ case VIR_NET_SSH_AUTH_KEYBOARD_INTERACTIVE:
+ if (strstr(auth_list, "keyboard-interactive"))
+ ret = virNetSSHAuthenticateKeyboardInteractive(sess, auth);
+ break;
+ case VIR_NET_SSH_AUTH_AGENT:
+ if (strstr(auth_list, "publickey"))
+ ret = virNetSSHAuthenticateAgent(sess, auth);
+ break;
+ case VIR_NET_SSH_AUTH_PRIVKEY:
+ if (strstr(auth_list, "publickey"))
+ ret = virNetSSHAuthenticatePrivkey(sess, auth);
+ break;
+ case VIR_NET_SSH_AUTH_PASSWORD:
+ if (strstr(auth_list, "password"))
+ ret = virNetSSHAuthenticatePassword(sess, auth);
+ break;
+ }
+
+ /* return on success or error */
+ if (ret <= 0)
+ return ret;
+
+ /* the authentication method is not supported */
+ if (ret == 2)
+ no_method = true;
+
+ /* authentication with this method has failed */
+ if (ret == 1)
+ auth_failed = true;
+ }
+
+ if (sess->nauths == 0) {
+ virReportError(VIR_ERR_AUTH_FAILED, "%s",
+ _("No authentication methods supplied"));
+ } else if (sess->nauths == 1) {
+ /* pass through the error */
+ } else if (no_method && !auth_failed) {
+ virReportError(VIR_ERR_AUTH_FAILED, "%s",
+ _("None of the requested authentication methods "
+ "are supported by the server"));
+ } else {
+ virReportError(VIR_ERR_AUTH_FAILED, "%s",
+ _("All provided authentication methods with credentials "
+ "were rejected by the server"));
+ }
+
+ return -1;
+}
+
+/* open channel */
+static int
+virNetSSHOpenChannel(virNetSSHSessionPtr sess)
+{
+ char *errmsg;
+
+ sess->channel = libssh2_channel_open_session(sess->session);
+ if (!sess->channel) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("failed to open ssh channel: %s"),
+ errmsg);
+ return -1;
+ }
+
+ if (libssh2_channel_exec(sess->channel, sess->channelCommand) != 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("failed to execute command '%s': %s"),
+ sess->channelCommand,
+ errmsg);
+ return -1;
+ }
+
+ /* nonblocking mode - currently does nothing*/
+ libssh2_channel_set_blocking(sess->channel, 0);
+
+ /* channel open */
+ return 0;
+}
+
+/* validate if all required parameters are configured */
+static int
+virNetSSHValidateConfig(virNetSSHSessionPtr sess)
+{
+ if (sess->nauths == 0) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("No authentication methods and credentials "
+ "provided"));
+ return -1;
+ }
+
+ if (!sess->channelCommand) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("No channel command provided"));
+ return -1;
+ }
+
+ if (sess->hostKeyVerify != VIR_NET_SSH_HOSTKEY_VERIFY_IGNORE) {
+ if (!sess->hostname) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Hostname is needed for host key verification"));
+ return -1;
+ }
+ }
+
+ /* everything ok */
+ return 0;
+}
+
+/* ### PUBLIC API ### */
+int
+virNetSSHSessionAuthSetCallback(virNetSSHSessionPtr sess,
+ virConnectAuthPtr auth)
+{
+ virMutexLock(&sess->lock);
+ sess->cred = auth;
+ virMutexUnlock(&sess->lock);
+ return 0;
+}
+
+void
+virNetSSHSessionAuthReset(virNetSSHSessionPtr sess)
+{
+ virMutexLock(&sess->lock);
+ virNetSSHSessionAuthMethodsFree(sess);
+ virMutexUnlock(&sess->lock);
+}
+
+int
+virNetSSHSessionAuthAddPasswordAuth(virNetSSHSessionPtr sess,
+ const char *username,
+ const char *password)
+{
+ virNetSSHAuthMethodPtr auth;
+ char *user = NULL;
+ char *pass = NULL;
+
+ if (!username || !password) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Username and password must be provided "
+ "for password authentication"));
+ return -1;
+ }
+
+ virMutexLock(&sess->lock);
+
+ if (!(user = strdup(username)) ||
+ !(pass = strdup(password)))
+ goto no_memory;
+
+ if (!(auth = virNetSSHSessionAuthMethodNew(sess)))
+ goto no_memory;
+
+ auth->username = user;
+ auth->password = pass;
+ auth->method = VIR_NET_SSH_AUTH_PASSWORD;
+
+ virMutexUnlock(&sess->lock);
+ return 0;
+
+no_memory:
+ VIR_FREE(user);
+ VIR_FREE(pass);
+ virReportOOMError();
+ virMutexUnlock(&sess->lock);
+ return -1;
+}
+
+int
+virNetSSHSessionAuthAddAgentAuth(virNetSSHSessionPtr sess,
+ const char *username)
+{
+ virNetSSHAuthMethodPtr auth;
+ char *user = NULL;
+
+ if (!username) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Username must be provided "
+ "for ssh agent authentication"));
+ return -1;
+ }
+
+ virMutexLock(&sess->lock);
+
+ if (!(user = strdup(username)))
+ goto no_memory;
+
+ if (!(auth = virNetSSHSessionAuthMethodNew(sess)))
+ goto no_memory;
+
+ auth->username = user;
+ auth->method = VIR_NET_SSH_AUTH_AGENT;
+
+ virMutexUnlock(&sess->lock);
+ return 0;
+
+no_memory:
+ VIR_FREE(user);
+ virReportOOMError();
+ virMutexUnlock(&sess->lock);
+ return -1;
+}
+
+int
+virNetSSHSessionAuthAddPrivKeyAuth(virNetSSHSessionPtr sess,
+ const char *username,
+ const char *keyfile,
+ const char *password)
+{
+ virNetSSHAuthMethodPtr auth;
+
+ char *user = NULL;
+ char *pass = NULL;
+ char *file = NULL;
+
+ if (!username || !keyfile) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Username and key file path must be provided "
+ "for private key authentication"));
+ return -1;
+ }
+
+ virMutexLock(&sess->lock);
+
+ if (!(user = strdup(username)) ||
+ !(file = strdup(keyfile)))
+ goto no_memory;
+
+ if (password && !(pass = strdup(password)))
+ goto no_memory;
+
+ if (!(auth = virNetSSHSessionAuthMethodNew(sess)))
+ goto no_memory;
+
+ auth->username = user;
+ auth->password = pass;
+ auth->filename = file;
+ auth->method = VIR_NET_SSH_AUTH_PRIVKEY;
+
+ virMutexUnlock(&sess->lock);
+ return 0;
+
+no_memory:
+ VIR_FREE(user);
+ VIR_FREE(pass);
+ VIR_FREE(file);
+ virReportOOMError();
+ virMutexUnlock(&sess->lock);
+ return -1;
+}
+
+int
+virNetSSHSessionAuthAddKeyboardAuth(virNetSSHSessionPtr sess,
+ const char *username,
+ int tries)
+{
+ virNetSSHAuthMethodPtr auth;
+ char *user = NULL;
+
+ if (!username) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Username must be provided "
+ "for ssh agent authentication"));
+ return -1;
+ }
+
+ virMutexLock(&sess->lock);
+
+ if (!(user = strdup(username)))
+ goto no_memory;
+
+ if (!(auth = virNetSSHSessionAuthMethodNew(sess)))
+ goto no_memory;
+
+ auth->username = user;
+ auth->tries = tries;
+ auth->method = VIR_NET_SSH_AUTH_KEYBOARD_INTERACTIVE;
+
+ virMutexUnlock(&sess->lock);
+ return 0;
+
+no_memory:
+ VIR_FREE(user);
+ virReportOOMError();
+ virMutexUnlock(&sess->lock);
+ return -1;
+
+}
+
+int
+virNetSSHSessionSetChannelCommand(virNetSSHSessionPtr sess,
+ const char *command)
+{
+ int ret = 0;
+ virMutexLock(&sess->lock);
+
+ VIR_FREE(sess->channelCommand);
+
+ if (command && !(sess->channelCommand = strdup(command))) {
+ virReportOOMError();
+ ret = -1;
+ }
+
+ virMutexUnlock(&sess->lock);
+ return ret;
+}
+
+int
+virNetSSHSessionSetHostKeyVerification(virNetSSHSessionPtr sess,
+ const char *hostname,
+ int port,
+ const char *hostsfile,
+ bool readonly,
+ virNetSSHHostkeyVerify opt)
+{
+ char *errmsg;
+
+ virMutexLock(&sess->lock);
+
+ sess->port = port;
+ sess->hostKeyVerify = opt;
+
+ VIR_FREE(sess->hostname);
+
+ if (hostname && !(sess->hostname = strdup(hostname)))
+ goto no_memory;
+
+ /* load the known hosts file */
+ if (hostsfile) {
+ if (libssh2_knownhost_readfile(sess->knownHosts,
+ hostsfile,
+ LIBSSH2_KNOWNHOST_FILE_OPENSSH) < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("unable to load knownhosts file '%s': %s"),
+ hostsfile, errmsg);
+ goto error;
+ }
+
+ /* set filename only if writing to the known hosts file is requested */
+
+ if (!readonly) {
+ VIR_FREE(sess->knownHostsFile);
+ if (!(sess->knownHostsFile = strdup(hostsfile)))
+ goto no_memory;
+ }
+ }
+
+ virMutexUnlock(&sess->lock);
+ return 0;
+
+no_memory:
+ virReportOOMError();
+error:
+ virMutexUnlock(&sess->lock);
+ return -1;
+}
+
+/* allocate and initialize a ssh session object */
+virNetSSHSessionPtr virNetSSHSessionNew(void)
+{
+ virNetSSHSessionPtr sess = NULL;
+
+ if (virNetSSHSessionInitialize() < 0)
+ goto error;
+
+ if (!(sess = virObjectNew(virNetSSHSessionClass)))
+ goto error;
+
+ /* initialize internal structures */
+ if (virMutexInit(&sess->lock) < 0) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Failed to initialize mutex"));
+ goto error;
+ }
+
+ /* initialize session data, use the internal data for callbacks
+ * and stick to default memory management functions */
+ if (!(sess->session = libssh2_session_init_ex(NULL,
+ NULL,
+ NULL,
+ (void *)sess))) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Failed to initialize libssh2 session"));
+ goto error;
+ }
+
+ if (!(sess->knownHosts = libssh2_knownhost_init(sess->session))) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Failed to initialize libssh2 known hosts table"));
+ goto error;
+ }
+
+ if (!(sess->agent = libssh2_agent_init(sess->session))) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Failed to initialize libssh2 agent handle"));
+ goto error;
+ }
+
+ VIR_DEBUG("virNetSSHSessionPtr: %p, LIBSSH2_SESSION: %p",
+ sess, sess->session);
+
+ /* set blocking mode for libssh2 until handshake is complete */
+ libssh2_session_set_blocking(sess->session, 1);
+
+ /* default states for config variables */
+ sess->state = VIR_NET_SSH_STATE_NEW;
+ sess->hostKeyVerify = VIR_NET_SSH_HOSTKEY_VERIFY_IGNORE;
+
+ return sess;
+
+error:
+ virObjectUnref(sess);
+ return NULL;
+}
+
+int
+virNetSSHSessionConnect(virNetSSHSessionPtr sess,
+ int sock)
+{
+ int ret;
+ char *errmsg;
+
+ VIR_DEBUG("sess=%p, sock=%d", sess, sock);
+
+ if (!sess || sess->state != VIR_NET_SSH_STATE_NEW) {
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Invalid virNetSSHSessionPtr"));
+ return -1;
+ }
+
+ virMutexLock(&sess->lock);
+
+ /* check if configuration is valid */
+ if ((ret = virNetSSHValidateConfig(sess)) < 0)
+ goto error;
+
+ /* open session */
+ ret = libssh2_session_handshake(sess->session, sock);
+ /* libssh2 is in blocking mode, so EAGAIN will never happen */
+ if (ret < 0) {
+ libssh2_session_last_error(sess->session, &errmsg, NULL, 0);
+ virReportError(VIR_ERR_NO_CONNECT,
+ _("SSH session handshake failed: %s"),
+ errmsg);
+ goto error;
+ }
+
+ /* verify the SSH host key */
+ if ((ret = virNetSSHCheckHostKey(sess)) != 0)
+ goto error;
+
+ /* authenticate */
+ if ((ret = virNetSSHAuthenticate(sess)) != 0)
+ goto error;
+
+ /* open channel */
+ if ((ret = virNetSSHOpenChannel(sess)) != 0)
+ goto error;
+
+ /* all set */
+ /* switch to nonblocking mode and return */
+ libssh2_session_set_blocking(sess->session, 0);
+ sess->state = VIR_NET_SSH_STATE_HANDSHAKE_COMPLETE;
+
+ virMutexUnlock(&sess->lock);
+ return ret;
+
+error:
+ sess->state = VIR_NET_SSH_STATE_ERROR;
+ virMutexUnlock(&sess->lock);
+ return ret;
+}
+
+/* do a read from a ssh channel, used instead of normal read on socket */
+ssize_t
+virNetSSHChannelRead(virNetSSHSessionPtr sess,
+ char *buf,
+ size_t len)
+{
+ ssize_t ret = -1;
+ ssize_t read_n = 0;
+
+ virMutexLock(&sess->lock);
+
+ if (sess->state != VIR_NET_SSH_STATE_HANDSHAKE_COMPLETE) {
+ if (sess->state == VIR_NET_SSH_STATE_ERROR_REMOTE)
+ virReportError(VIR_ERR_SSH,
+ _("Remote program terminated "
+ "with non-zero code: %d"),
+ sess->channelCommandReturnValue);
+ else
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Tried to write socket in error state"));
+
+ virMutexUnlock(&sess->lock);
+ return -1;
+ }
+
+ if (sess->bufUsed > 0) {
+ /* copy the rest (or complete) internal buffer to the output buffer */
+ memcpy(buf,
+ sess->rbuf + sess->bufStart,
+ len > sess->bufUsed ? sess->bufUsed : len);
+
+ if (len >= sess->bufUsed) {
+ read_n = sess->bufUsed;
+
+ sess->bufStart = 0;
+ sess->bufUsed = 0;
+ } else {
+ read_n = len;
+ sess->bufUsed -= len;
+ sess->bufStart += len;
+
+ goto success;
+ }
+ }
+
+ /* continue reading into the buffer supplied */
+ if (read_n < len) {
+ ret = libssh2_channel_read(sess->channel,
+ buf + read_n,
+ len - read_n);
+
+ if (ret == LIBSSH2_ERROR_EAGAIN)
+ goto success;
+
+ if (ret < 0)
+ goto error;
+
+ read_n += ret;
+ }
+
+ /* try to read something into the internal buffer */
+ if (sess->bufUsed == 0) {
+ ret = libssh2_channel_read(sess->channel,
+ sess->rbuf,
+ VIR_NET_SSH_BUFFER_SIZE);
+
+ if (ret == LIBSSH2_ERROR_EAGAIN)
+ goto success;
+
+ if (ret < 0)
+ goto error;
+
+ sess->bufUsed = ret;
+ sess->bufStart = 0;
+ }
+
+ if (read_n == 0) {
+ /* get rid of data in stderr stream */
+ ret = libssh2_channel_read_stderr(sess->channel,
+ sess->rbuf,
+ VIR_NET_SSH_BUFFER_SIZE - 1);
+ if (ret > 0) {
+ sess->rbuf[ret] = '\0';
+ VIR_DEBUG("flushing stderr, data='%s'", sess->rbuf);
+ }
+ }
+
+ if (libssh2_channel_eof(sess->channel)) {
+ if (libssh2_channel_get_exit_status(sess->channel)) {
+ virReportError(VIR_ERR_SSH,
+ _("Remote command terminated with non-zero code: %d"),
+ libssh2_channel_get_exit_status(sess->channel));
+ sess->channelCommandReturnValue = libssh2_channel_get_exit_status(sess->channel);
+ sess->state = VIR_NET_SSH_STATE_ERROR_REMOTE;
+ virMutexUnlock(&sess->lock);
+ return -1;
+ }
+
+ sess->state = VIR_NET_SSH_STATE_CLOSED;
+ virMutexUnlock(&sess->lock);
+ return -1;
+ }
+
+success:
+ virMutexUnlock(&sess->lock);
+ return read_n;
+
+error:
+ sess->state = VIR_NET_SSH_STATE_ERROR;
+ virMutexUnlock(&sess->lock);
+ return ret;
+}
+
+ssize_t
+virNetSSHChannelWrite(virNetSSHSessionPtr sess,
+ const char *buf,
+ size_t len)
+{
+ ssize_t ret;
+
+ virMutexLock(&sess->lock);
+
+ if (sess->state != VIR_NET_SSH_STATE_HANDSHAKE_COMPLETE) {
+ if (sess->state == VIR_NET_SSH_STATE_ERROR_REMOTE)
+ virReportError(VIR_ERR_SSH,
+ _("Remote program terminated with non-zero code: %d"),
+ sess->channelCommandReturnValue);
+ else
+ virReportError(VIR_ERR_SSH, "%s",
+ _("Tried to write socket in error state"));
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (libssh2_channel_eof(sess->channel)) {
+ if (libssh2_channel_get_exit_status(sess->channel)) {
+ virReportError(VIR_ERR_SSH,
+ _("Remote program terminated with non-zero code: %d"),
+ libssh2_channel_get_exit_status(sess->channel));
+ sess->state = VIR_NET_SSH_STATE_ERROR_REMOTE;
+ sess->channelCommandReturnValue = libssh2_channel_get_exit_status(sess->channel);
+
+ ret = -1;
+ goto cleanup;
+ }
+
+ sess->state = VIR_NET_SSH_STATE_CLOSED;
+ ret = -1;
+ goto cleanup;
+ }
+
+ ret = libssh2_channel_write(sess->channel, buf, len);
+ if (ret == LIBSSH2_ERROR_EAGAIN) {
+ ret = 0;
+ goto cleanup;
+ }
+
+ if (ret < 0) {
+ char *msg;
+ sess->state = VIR_NET_SSH_STATE_ERROR;
+ libssh2_session_last_error(sess->session, &msg, NULL, 0);
+ virReportError(VIR_ERR_SSH,
+ _("write failed: %s"), msg);
+ }
+
+cleanup:
+ virMutexUnlock(&sess->lock);
+ return ret;
+}
+
+bool
+virNetSSHSessionHasCachedData(virNetSSHSessionPtr sess)
+{
+ bool ret;
+
+ if (!sess)
+ return false;
+
+ virMutexLock(&sess->lock);
+
+ ret = sess->bufUsed > 0;
+
+ virMutexUnlock(&sess->lock);
+ return ret;
+}