/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit 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.
*
* Cockpit 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 Cockpit; If not, see .
*/
#include "config.h"
#include "common/cockpitauthorize.h"
#include "common/cockpitconf.h"
#include "common/cockpithex.h"
#include "common/cockpitframe.h"
#include "common/cockpitjson.h"
#include "common/cockpitmemory.h"
#include "common/cockpitpipe.h"
#include "common/cockpitlog.h"
#include "common/cockpittest.h"
#include "common/cockpittransport.h"
#include "common/cockpitunixfd.h"
#include "common/cockpitknownhosts.h"
#include "cockpitsshrelay.h"
#include "cockpitsshoptions.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* we had a private one before moving to /etc/ssh/ssh_known_hosts */
#define LEGACY_KNOWN_HOSTS PACKAGE_LOCALSTATE_DIR "/known_hosts"
typedef struct {
const gchar *logname;
gchar *initial_auth_data;
gchar *auth_type;
gchar **env;
CockpitSshOptions *ssh_options;
gchar *username;
gboolean in_bridge;
ssh_session session;
gchar *conversation;
gchar *host_key;
gchar *host_fingerprint;
const gchar *host_key_type;
GHashTable *auth_results;
gchar *user_known_hosts;
gint outfd;
} CockpitSshData;
static gchar *tmp_knownhost_file;
static const gchar*
exit_code_problem (int exit_code)
{
switch (exit_code)
{
case 0:
return NULL;
case AUTHENTICATION_FAILED:
return "authentication-failed";
case DISCONNECTED:
return "disconnected";
case TERMINATED:
return "terminated";
case NO_COCKPIT:
return "no-cockpit";
default:
return "internal-error";
}
}
static const gchar *
auth_method_description (int method)
{
if (method == SSH_AUTH_METHOD_NONE)
return "none";
else if (method == SSH_AUTH_METHOD_PASSWORD || method == SSH_AUTH_METHOD_INTERACTIVE)
return "password";
else if (method == SSH_AUTH_METHOD_PUBLICKEY)
return "public-key";
else if (method == SSH_AUTH_METHOD_HOSTBASED)
return "host-based";
else if (method == SSH_AUTH_METHOD_GSSAPI_MIC)
return "gssapi-mic";
else
return "unknown";
}
static gchar *
auth_methods_line (int methods)
{
GString *string;
int i = 0;
int check[6] = {
SSH_AUTH_METHOD_NONE,
SSH_AUTH_METHOD_INTERACTIVE,
SSH_AUTH_METHOD_PASSWORD,
SSH_AUTH_METHOD_PUBLICKEY,
SSH_AUTH_METHOD_HOSTBASED,
SSH_AUTH_METHOD_GSSAPI_MIC
};
string = g_string_new ("");
for (i = 0; i < G_N_ELEMENTS (check); i++)
{
if (methods & check[i])
{
g_string_append (string, auth_method_description (check[i]));
g_string_append (string, " ");
}
}
return g_string_free (string, FALSE);
}
static gboolean
ssh_msg_is_disconnected (const gchar *msg)
{
return msg && (strstr (msg, "disconnected") ||
strstr (msg, "SSH_MSG_DISCONNECT") ||
strstr (msg, "Socket error: Success") ||
strstr (msg, "Socket error: Connection reset by peer"));
}
static gboolean
write_control_message (int fd,
JsonObject *options)
{
gboolean ret = TRUE;
gchar *payload;
gchar *prefixed;
gsize length;
payload = cockpit_json_write_object (options, &length);
prefixed = g_strdup_printf ("\n%s", payload);
if (cockpit_frame_write (fd, (unsigned char *)prefixed, length + 1) < 0)
{
g_message ("couldn't write control message: %s", g_strerror (errno));
ret = FALSE;
}
g_free (prefixed);
g_free (payload);
return ret;
}
static void
byte_array_clear_and_free (gpointer data)
{
GByteArray *buffer = data;
cockpit_memory_clear (buffer->data, buffer->len);
g_byte_array_free (buffer, TRUE);
}
static JsonObject *
read_control_message (int fd)
{
JsonObject *options = NULL;
GBytes *payload = NULL;
GBytes *bytes = NULL;
gchar *channel = NULL;
guchar *data = NULL;
gssize length = 0;
length = cockpit_frame_read (fd, &data);
if (length < 0)
{
g_message ("couldn't read control message: %s", g_strerror (errno));
length = 0;
}
else if (length > 0)
{
/* This could have a password, so clear it when freeing */
bytes = g_bytes_new_with_free_func (data, length, byte_array_clear_and_free,
g_byte_array_new_take (data, length));
payload = cockpit_transport_parse_frame (bytes, &channel);
data = NULL;
}
if (payload == NULL)
{
if (length > 0)
g_message ("cockpit-ssh did not receive valid message");
}
else if (channel != NULL)
{
g_message ("cockpit-ssh did not receive a control message");
}
else if (!cockpit_transport_parse_command (payload, NULL, NULL, &options))
{
g_message ("cockpit-ssh did not receive a valid control message");
}
g_free (channel);
if (bytes)
g_bytes_unref (bytes);
if (payload)
g_bytes_unref (payload);
free (data);
return options;
}
static void
send_authorize_challenge (const gchar *challenge,
gint outfd)
{
gchar *cookie = NULL;
JsonObject *object = json_object_new ();
cookie = g_strdup_printf ("session%u%u",
(unsigned int)getpid(),
(unsigned int)time (NULL));
json_object_set_string_member (object, "command", "authorize");
json_object_set_string_member (object, "challenge", challenge);
json_object_set_string_member (object, "cookie", cookie);
write_control_message (outfd, object);
g_free (cookie);
json_object_unref (object);
}
static gchar *
challenge_for_auth_data (const gchar *challenge,
gint outfd,
gchar **ret_type)
{
const gchar *response = NULL;
const gchar *command;
gchar *ptr = NULL;
gchar *type = NULL;
JsonObject *reply;
send_authorize_challenge (challenge ? challenge : "*", outfd);
reply = read_control_message (STDIN_FILENO);
if (!reply)
goto out;
if (!cockpit_json_get_string (reply, "command", "", &command) ||
!g_str_equal (command, "authorize"))
{
g_message ("received \"%s\" control message instead of \"authorize\"", command);
}
else if (!cockpit_json_get_string (reply, "response", NULL, &response))
{
g_message ("received unexpected \"authorize\" control message: %s", response);
}
if (response)
cockpit_authorize_type (response, &type);
out:
if (ret_type)
*ret_type = type;
else
g_free (type);
if (response && !g_str_equal (response, ""))
ptr = g_strdup (response);
if (reply)
json_object_unref (reply);
return ptr;
}
static gchar *
challenge_for_knownhosts_data (CockpitSshData *data)
{
const gchar *value = NULL;
gchar *ret = NULL;
gchar *response = NULL;
response = challenge_for_auth_data ("x-host-key", data->outfd, NULL);
if (response)
{
value = cockpit_authorize_type (response, NULL);
/* Legacy blank string means force fail */
if (value && value[0] == '\0')
ret = g_strdup ("* invalid key");
else
ret = g_strdup (value);
}
g_free (response);
return ret;
}
static gchar *
prompt_with_authorize (CockpitSshData *data,
const gchar *prompt,
const gchar *msg,
const gchar *default_value,
const gchar *host_key,
gboolean echo)
{
JsonObject *request = NULL;
JsonObject *reply = NULL;
const gchar *command = NULL;
const char *response = NULL;
char *challenge = NULL;
gchar *result = NULL;
gboolean ret;
challenge = cockpit_authorize_build_x_conversation (prompt, &data->conversation);
if (!challenge)
return NULL;
request = json_object_new ();
json_object_set_string_member (request, "command", "authorize");
json_object_set_string_member (request, "cookie", data->conversation);
json_object_set_string_member (request, "challenge", challenge);
cockpit_memory_clear (challenge, -1);
free (challenge);
if (msg)
json_object_set_string_member (request, "message", msg);
if (default_value)
json_object_set_string_member (request, "default", default_value);
if (host_key)
json_object_set_string_member (request, "host-key", host_key);
json_object_set_boolean_member (request, "echo", echo);
ret = write_control_message (data->outfd, request);
json_object_unref (request);
if (!ret)
return NULL;
reply = read_control_message (STDIN_FILENO);
if (!reply)
return NULL;
if (!cockpit_json_get_string (reply, "command", "", &command) ||
!g_str_equal (command, "authorize"))
{
g_message ("received \"%s\" control message instead of \"authorize\"", command);
}
else if (!cockpit_json_get_string (reply, "response", "", &response))
{
g_message ("received unexpected \"authorize\" control message");
}
else if (!g_str_equal (response, ""))
{
result = cockpit_authorize_parse_x_conversation (response, NULL);
if (!result)
g_message ("received unexpected \"authorize\" control message \"response\"");
}
json_object_unref (reply);
return result;
}
/*
* HACK: SELinux prevents us from writing to the directories we want to
* write to, so we have to try multiple locations.
*
* https://bugzilla.redhat.com/show_bug.cgi?id=1279430
*/
static gchar *
create_knownhosts_temp (void)
{
const gchar *directories[] = {
"/tmp",
PACKAGE_LOCALSTATE_DIR,
NULL,
};
gchar *name;
int i, fd, err;
for (i = 0; directories[i] != NULL; i++)
{
name = g_build_filename (directories[i], "known-hosts.XXXXXX", NULL);
fd = g_mkstemp (name);
err = errno;
if (fd >= 0)
{
close (fd);
return name;
}
g_free (name);
if ((err == ENOENT || err == EPERM || err == EACCES) && directories[i + 1] != NULL)
continue;
g_warning ("couldn't make temporary file for knownhosts line in %s: %m", directories[i]);
break;
}
return NULL;
}
/*
* NOTE: This function changes the SSH_OPTIONS_KNOWNHOSTS option on
* the session.
*
* We can't save and restore it since ssh_options_get doesn't allow us
* to retrieve the old value of SSH_OPTIONS_KNOWNHOSTS.
*
* HACK: This function should be provided by libssh.
*
* https://red.libssh.org/issues/162
*/
static gchar *
get_knownhosts_line (ssh_session session)
{
gchar *name = NULL;
GError *error = NULL;
gchar *line = NULL;
name = create_knownhosts_temp ();
if (!name)
goto out;
if (ssh_options_set (session, SSH_OPTIONS_KNOWNHOSTS, name) != SSH_OK)
{
g_warning ("Couldn't set SSH_OPTIONS_KNOWNHOSTS option.");
goto out;
}
if (ssh_write_knownhost (session) != SSH_OK)
{
g_warning ("Couldn't write knownhosts file: %s", ssh_get_error (session));
goto out;
}
if (!g_file_get_contents (name, &line, NULL, &error))
{
g_warning ("Couldn't read temporary known_hosts %s: %s", name, error->message);
g_clear_error (&error);
goto out;
}
g_strstrip (line);
out:
if (name)
{
g_unlink (name);
g_free (name);
}
return line;
}
static const gchar *
prompt_for_host_key (CockpitSshData *data)
{
const gchar *ret;
gchar *host = NULL;
guint port = 22;
gchar *message = NULL;
gchar *prompt = NULL;
gchar *reply = NULL;
if (ssh_options_get (data->session, SSH_OPTIONS_HOST, &host) < 0)
{
g_warning ("Failed to get host");
goto out;
}
if (ssh_options_get_port (data->session, &port) < 0)
{
g_warning ("Failed to get port");
goto out;
}
message = g_strdup_printf ("The authenticity of host '%s:%d' can't be established. Do you want to proceed this time?",
host, port);
prompt = g_strdup_printf ("MD5 Fingerprint (%s):", data->host_key_type);
reply = prompt_with_authorize (data, prompt, message, data->host_fingerprint, data->host_key, TRUE);
out:
if (g_strcmp0 (reply, data->host_fingerprint) == 0 || g_strcmp0 (reply, data->host_key) == 0)
ret = NULL;
else
ret = "unknown-hostkey";
g_free (reply);
g_free (message);
g_free (prompt);
g_free (host);
return ret;
}
static void cleanup_knownhosts_file (void)
{
if (tmp_knownhost_file)
{
g_unlink (tmp_knownhost_file);
g_free (tmp_knownhost_file);
}
}
/**
* set_knownhosts_file:
*
* Check the various ssh known hosts locations and set the appropriate one into
* SSH_OPTIONS_KNOWNHOSTS.
*
* Returns: error string or %NULL on success.
*/
static const gchar *
set_knownhosts_file (CockpitSshData *data,
const gchar* host,
const guint port)
{
gboolean host_known;
const gchar *knownhosts_data;
const gchar *problem;
gchar *authorize_knownhosts_data = NULL;
if (data->ssh_options->knownhosts_authorize)
{
authorize_knownhosts_data = challenge_for_knownhosts_data (data);
knownhosts_data = authorize_knownhosts_data;
}
else
{
knownhosts_data = data->ssh_options->knownhosts_data;
}
/* $COCKPIT_SSH_KNOWN_HOSTS_DATA has highest priority */
if (knownhosts_data)
{
FILE *fp = NULL;
tmp_knownhost_file = create_knownhosts_temp ();
if (!tmp_knownhost_file)
{
problem = "internal-error";
goto out;
}
atexit (cleanup_knownhosts_file);
fp = fopen (tmp_knownhost_file, "a");
if (fp == NULL)
{
g_warning ("%s: couldn't open temporary known host file for data: %s",
data->logname, tmp_knownhost_file);
problem = "internal-error";
goto out;
}
if (fputs (knownhosts_data, fp) < 0)
{
g_warning ("%s: couldn't write to data to temporary known host file: %s",
data->logname, g_strerror (errno));
fclose (fp);
problem = "internal-error";
goto out;
}
fclose (fp);
data->ssh_options->knownhosts_file = tmp_knownhost_file;
}
/* now check the default global ssh file */
host_known = cockpit_is_host_known (data->ssh_options->knownhosts_file, host, port);
/* if we check the default system known hosts file (i. e. not during the test suite), also check
* the legacy file in /var/lib/cockpit and the user's ssh; we need to do that even with
* allow_unknown_hosts as subsequent code relies on knownhosts_file */
if (!host_known && strcmp (data->ssh_options->knownhosts_file, cockpit_get_default_knownhosts ()) == 0)
{
host_known = cockpit_is_host_known (LEGACY_KNOWN_HOSTS, host, port);
if (host_known)
{
g_debug ("%s: not known in %s but in legacy file %s",
data->logname,
data->ssh_options->knownhosts_file,
LEGACY_KNOWN_HOSTS);
data->ssh_options->knownhosts_file = LEGACY_KNOWN_HOSTS;
}
/* check ~/.ssh/known_hosts, unless we are running as a system user ($HOME == "/"); this is not
* a security check (if one can write /.ssh/known_hosts then we have to trust them), just caution */
if (!host_known && g_strcmp0 (g_get_home_dir (), "/") != 0)
{
host_known = cockpit_is_host_known (data->user_known_hosts, host, port);
if (host_known)
data->ssh_options->knownhosts_file = data->user_known_hosts;
}
}
g_debug ("%s: using known hosts file %s", data->logname, data->ssh_options->knownhosts_file);
if (!data->ssh_options->allow_unknown_hosts && !host_known)
{
g_message ("%s: refusing to connect to unknown host: %s:%d",
data->logname, host, port);
problem = "unknown-host";
goto out;
}
problem = NULL;
out:
g_free (authorize_knownhosts_data);
return problem;
}
static const gchar *
verify_knownhost (CockpitSshData *data,
const gchar* host,
const guint port)
{
const gchar *ret = "invalid-hostkey";
ssh_key key = NULL;
unsigned char *hash = NULL;
int state;
gsize len;
data->host_key = get_knownhosts_line (data->session);
if (data->host_key == NULL)
{
ret = "internal-error";
goto done;
}
#ifdef HAVE_SSH_GET_SERVER_PUBLICKEY
if (ssh_get_server_publickey (data->session, &key) != SSH_OK)
#else
if (ssh_get_publickey (data->session, &key) != SSH_OK)
#endif
{
g_warning ("Couldn't look up ssh host key");
ret = "internal-error";
goto done;
}
data->host_key_type = ssh_key_type_to_char (ssh_key_type (key));
if (data->host_key_type == NULL)
{
g_warning ("Couldn't lookup host key type");
ret = "internal-error";
goto done;
}
if (ssh_get_publickey_hash (key, SSH_PUBLICKEY_HASH_MD5, &hash, &len) < 0)
{
g_warning ("Couldn't hash ssh public key");
ret = "internal-error";
goto done;
}
else
{
data->host_fingerprint = ssh_get_hexa (hash, len);
ssh_clean_pubkey_hash (&hash);
}
if (ssh_options_set (data->session, SSH_OPTIONS_KNOWNHOSTS,
data->ssh_options->knownhosts_file) != SSH_OK)
{
g_warning ("Couldn't set knownhosts file location");
ret = "internal-error";
goto done;
}
state = ssh_is_server_known (data->session);
if (state == SSH_SERVER_KNOWN_OK)
{
g_debug ("%s: verified host key", data->logname);
ret = NULL; /* success */
goto done;
}
else if (state == SSH_SERVER_ERROR)
{
g_warning ("%s: couldn't check host key: %s", data->logname,
ssh_get_error (data->session));
ret = "internal-error";
goto done;
}
switch (state)
{
case SSH_SERVER_KNOWN_OK:
case SSH_SERVER_ERROR:
g_assert_not_reached ();
break;
case SSH_SERVER_KNOWN_CHANGED:
g_message ("%s: %s host key for server has changed to: %s",
data->logname, data->host_key_type, data->host_fingerprint);
break;
case SSH_SERVER_FOUND_OTHER:
g_message ("%s: host key for this server changed key type: %s",
data->logname, data->host_key_type);
break;
case SSH_SERVER_FILE_NOT_FOUND:
g_debug ("Couldn't find the known hosts file");
/* fall through */
case SSH_SERVER_NOT_KNOWN:
ret = prompt_for_host_key (data);
if (ret)
{
g_message ("%s: %s host key for server is not known: %s",
data->logname, data->host_key_type, data->host_fingerprint);
}
break;
}
done:
if (key)
ssh_key_free (key);
return ret;
}
static const gchar *
auth_result_string (int rc)
{
switch (rc)
{
case SSH_AUTH_SUCCESS:
return "succeeded";
case SSH_AUTH_DENIED:
return "denied";
case SSH_AUTH_PARTIAL:
return "partial";
break;
case SSH_AUTH_AGAIN:
return "again";
default:
return "error";
}
}
static gchar *
parse_auth_password (const gchar *auth_type,
const gchar *auth_data)
{
gchar *password = NULL;
g_assert (auth_data != NULL);
g_assert (auth_type != NULL);
if (g_strcmp0 (auth_type, "basic") == 0)
password = cockpit_authorize_parse_basic (auth_data, NULL);
else
password = g_strdup (cockpit_authorize_type (auth_data, NULL));
if (password == NULL)
password = g_strdup ("");
return password;
}
static int
do_interactive_auth (CockpitSshData *data)
{
int rc;
gboolean sent_pw = FALSE;
gchar *password = NULL;
password = parse_auth_password (data->auth_type,
data->initial_auth_data);
rc = ssh_userauth_kbdint (data->session, NULL, NULL);
while (rc == SSH_AUTH_INFO)
{
const gchar *msg;
int n, i;
msg = ssh_userauth_kbdint_getinstruction (data->session);
n = ssh_userauth_kbdint_getnprompts (data->session);
for (i = 0; i < n && rc == SSH_AUTH_INFO; i++)
{
const char *prompt;
char *answer = NULL;
char echo = '\0';
int status = 0;
prompt = ssh_userauth_kbdint_getprompt (data->session, i, &echo);
g_debug ("%s: Got prompt %s prompt", data->logname, prompt);
if (!sent_pw)
{
status = ssh_userauth_kbdint_setanswer (data->session, i, password);
sent_pw = TRUE;
}
else
{
answer = prompt_with_authorize (data, prompt, msg, NULL, NULL, echo != '\0');
if (answer)
status = ssh_userauth_kbdint_setanswer (data->session, i, answer);
else
rc = SSH_AUTH_ERROR;
g_free (answer);
}
if (status < 0)
{
g_warning ("%s: failed to set answer for %s", data->logname, prompt);
rc = SSH_AUTH_ERROR;
}
}
if (rc == SSH_AUTH_INFO)
rc = ssh_userauth_kbdint (data->session, NULL, NULL);
}
cockpit_memory_clear (password, strlen (password));
g_free (password);
return rc;
}
static int
do_password_auth (CockpitSshData *data)
{
gchar *password = NULL;
const gchar *msg;
int rc;
password = parse_auth_password (data->auth_type,
data->initial_auth_data);
rc = ssh_userauth_password (data->session, NULL, password);
switch (rc)
{
case SSH_AUTH_SUCCESS:
g_debug ("%s: password auth succeeded", data->logname);
break;
case SSH_AUTH_DENIED:
g_debug ("%s: password auth failed", data->logname);
break;
case SSH_AUTH_PARTIAL:
g_message ("%s: password auth worked, but server wants more authentication",
data->logname);
break;
case SSH_AUTH_AGAIN:
g_message ("%s: password auth failed: server asked for retry",
data->logname);
break;
default:
msg = ssh_get_error (data->session);
g_message ("%s: couldn't authenticate: %s", data->logname, msg);
}
cockpit_memory_clear (password, strlen (password));
g_free (password);
return rc;
}
static int
do_key_auth (CockpitSshData *data)
{
int rc;
const gchar *msg;
const gchar *key_data;
ssh_key key;
g_assert (data->initial_auth_data != NULL);
key_data = cockpit_authorize_type (data->initial_auth_data, NULL);
if (!key_data)
{
g_message ("%s: Got invalid private-key data, %s", data->logname, data->initial_auth_data);
return SSH_AUTH_DENIED;
}
rc = ssh_pki_import_privkey_base64 (key_data, NULL, NULL, NULL, &key);
if (rc != SSH_OK)
{
g_message ("%s: Got invalid key data, %s", data->logname, data->initial_auth_data);
return rc;
}
rc = ssh_userauth_publickey (data->session, NULL, key);
switch (rc)
{
case SSH_AUTH_SUCCESS:
g_debug ("%s: key auth succeeded", data->logname);
break;
case SSH_AUTH_DENIED:
g_debug ("%s: key auth failed", data->logname);
break;
case SSH_AUTH_PARTIAL:
g_message ("%s: key auth worked, but server wants more authentication",
data->logname);
break;
case SSH_AUTH_AGAIN:
g_message ("%s: key auth failed: server asked for retry",
data->logname);
break;
default:
msg = ssh_get_error (data->session);
g_message ("%s: couldn't key authenticate: %s", data->logname, msg);
}
ssh_key_free (key);
return rc;
}
static int
do_agent_auth (CockpitSshData *data)
{
int rc;
const gchar *msg;
rc = ssh_userauth_agent (data->session, NULL);
switch (rc)
{
case SSH_AUTH_SUCCESS:
g_debug ("%s: agent auth succeeded", data->logname);
break;
case SSH_AUTH_DENIED:
g_debug ("%s: agent auth failed", data->logname);
break;
case SSH_AUTH_PARTIAL:
g_message ("%s: agent auth worked, but server wants more authentication",
data->logname);
break;
case SSH_AUTH_AGAIN:
g_message ("%s: agent auth failed: server asked for retry",
data->logname);
break;
default:
msg = ssh_get_error (data->session);
/*
HACK: https://red.libssh.org/issues/201
libssh returns error instead of denied
when agent has no keys. For now treat as
denied.
*/
if (strstr (msg, "Access denied"))
rc = SSH_AUTH_DENIED;
else
g_message ("%s: couldn't agent authenticate: %s", data->logname, msg);
}
return rc;
}
static int
do_gss_auth (CockpitSshData *data)
{
int rc;
const gchar *msg;
rc = ssh_userauth_gssapi (data->session);
switch (rc)
{
case SSH_AUTH_SUCCESS:
g_debug ("%s: gssapi auth succeeded", data->logname);
break;
case SSH_AUTH_DENIED:
g_debug ("%s: gssapi auth failed", data->logname);
break;
case SSH_AUTH_PARTIAL:
g_message ("%s: gssapi auth worked, but server wants more authentication",
data->logname);
break;
default:
msg = ssh_get_error (data->session);
g_message ("%s: couldn't authenticate: %s", data->logname, msg);
}
return rc;
}
static gboolean
has_password (CockpitSshData *data)
{
if (data->auth_type == NULL &&
data->initial_auth_data == NULL)
{
data->initial_auth_data = challenge_for_auth_data ("basic",
data->outfd,
&data->auth_type);
}
return (data->initial_auth_data != NULL &&
(g_strcmp0 (data->auth_type, "basic") == 0 ||
g_strcmp0 (data->auth_type, "password") == 0));
}
static const gchar *
cockpit_ssh_authenticate (CockpitSshData *data)
{
const gchar *problem;
gboolean have_final_result = FALSE;
gchar *description;
const gchar *msg;
int rc;
int methods_server;
int methods_tried = 0;
int methods_to_try = SSH_AUTH_METHOD_INTERACTIVE |
SSH_AUTH_METHOD_GSSAPI_MIC |
SSH_AUTH_METHOD_PUBLICKEY;
problem = "authentication-failed";
rc = ssh_userauth_none (data->session, NULL);
if (rc == SSH_AUTH_ERROR)
{
g_message ("%s: server authentication handshake failed: %s",
data->logname, ssh_get_error (data->session));
problem = "internal-error";
goto out;
}
if (rc == SSH_AUTH_SUCCESS)
{
problem = NULL;
goto out;
}
methods_server = ssh_userauth_list (data->session, NULL);
/* If interactive isn't supported try password instead */
if (!(methods_server & SSH_AUTH_METHOD_INTERACTIVE))
{
methods_to_try = methods_to_try | SSH_AUTH_METHOD_PASSWORD;
methods_to_try = methods_to_try & ~SSH_AUTH_METHOD_INTERACTIVE;
}
while (methods_to_try != 0)
{
int (*auth_func)(CockpitSshData *data);
const gchar *result_string;
int method;
gboolean has_creds = FALSE;
if (methods_to_try & SSH_AUTH_METHOD_PUBLICKEY)
{
method = SSH_AUTH_METHOD_PUBLICKEY;
if (g_strcmp0 (data->auth_type, "private-key") == 0)
{
auth_func = do_key_auth;
has_creds = data->initial_auth_data != NULL;
}
else
{
auth_func = do_agent_auth;
has_creds = TRUE;
}
}
else if (methods_to_try & SSH_AUTH_METHOD_INTERACTIVE)
{
auth_func = do_interactive_auth;
method = SSH_AUTH_METHOD_INTERACTIVE;
has_creds = has_password(data);
}
else if (methods_to_try & SSH_AUTH_METHOD_PASSWORD)
{
auth_func = do_password_auth;
method = SSH_AUTH_METHOD_PASSWORD;
has_creds = has_password(data);
}
else
{
auth_func = do_gss_auth;
method = SSH_AUTH_METHOD_GSSAPI_MIC;
has_creds = TRUE;
}
methods_to_try = methods_to_try & ~method;
if (!(methods_server & method))
{
result_string = "no-server-support";
}
else if (!has_creds)
{
result_string = "not-provided";
methods_tried = methods_tried | method;
}
else
{
methods_tried = methods_tried | method;
if (!have_final_result)
{
rc = auth_func (data);
result_string = auth_result_string (rc);
if (rc == SSH_AUTH_SUCCESS)
{
have_final_result = TRUE;
problem = NULL;
}
else if (rc == SSH_AUTH_ERROR)
{
have_final_result = TRUE;
msg = ssh_get_error (data->session);
g_message ("%s: couldn't authenticate: %s", data->logname, msg);
if (ssh_msg_is_disconnected (msg))
problem = "terminated";
else
problem = "internal-error";
}
}
else
{
result_string = "not-tried";
}
}
g_hash_table_insert (data->auth_results,
g_strdup (auth_method_description (method)),
g_strdup (result_string));
}
if (have_final_result)
goto out;
if (methods_tried == 0)
{
if (methods_server == 0)
{
g_message ("%s: server offered no authentication methods", data->logname);
}
else
{
description = auth_methods_line (methods_server);
g_message ("%s: server offered unsupported authentication methods: %s",
data->logname, description);
g_free (description);
}
}
out:
return problem;
}
static gboolean
send_auth_reply (CockpitSshData *data,
const gchar *problem)
{
GHashTableIter auth_iter;
JsonObject *auth_json = NULL; // consumed by object
JsonObject *object = NULL;
gboolean ret;
gpointer hkey;
gpointer hvalue;
object = json_object_new ();
auth_json = json_object_new ();
g_assert (problem != NULL);
json_object_set_string_member (object, "command", "init");
if (data->host_key)
json_object_set_string_member (object, "host-key", data->host_key);
if (data->host_fingerprint)
json_object_set_string_member (object, "host-fingerprint", data->host_fingerprint);
if (g_strcmp0 (problem, "invalid-hostkey") == 0 &&
tmp_knownhost_file == NULL)
{
json_object_set_string_member (object, "invalid-hostkey-file",
data->ssh_options->knownhosts_file);
}
json_object_set_string_member (object, "problem", problem);
json_object_set_string_member (object, "error", problem);
if (data->auth_results)
{
g_hash_table_iter_init (&auth_iter, data->auth_results);
while (g_hash_table_iter_next (&auth_iter, &hkey, &hvalue))
json_object_set_string_member (auth_json, hkey, hvalue);
}
json_object_set_object_member (object, "auth-method-results", auth_json);
ret = write_control_message (data->outfd, object);
json_object_unref (object);
if (!ret)
g_message ("couldn't write authorize message: %s", g_strerror (errno));
return ret;
}
static void
parse_host (const gchar *host,
gchar **hostname,
gchar **username,
guint *port)
{
gchar *user_arg = NULL;
gchar *host_arg = NULL;
gchar *tmp = NULL;
gchar *end = NULL;
guint64 tmp_num;
gsize host_offset = 0;
gsize host_length = strlen (host);
tmp = strrchr (host, '@');
if (tmp)
{
if (tmp[0] != host[0])
{
user_arg = g_strndup (host, tmp - host);
host_offset = strlen (user_arg) + 1;
host_length = host_length - host_offset;
}
else
{
g_message ("ignoring blank user in %s", host);
}
}
tmp = strrchr (host, ':');
if (tmp)
{
tmp_num = g_ascii_strtoull (tmp + 1, &end, 10);
if (end[0] == '\0' && tmp_num > 0 && tmp_num < G_MAXUSHORT)
{
*port = (guint) tmp_num;
host_length = host_length - strlen (tmp);
}
else
{
g_message ("ignoring invalid port in %s", host);
}
}
if (!user_arg || user_arg[0] == '\0')
user_arg = (gchar *) g_get_user_name ();
host_arg = g_strndup (host + host_offset, host_length);
*hostname = g_strdup (host_arg);
*username = g_strdup (user_arg);
g_free (host_arg);
g_free (user_arg);
}
static gchar *
username_from_basic (const gchar *basic_data)
{
gchar *user = NULL;
gchar *password;
password = cockpit_authorize_parse_basic (basic_data, &user);
if (password)
{
cockpit_memory_clear (password, -1);
free (password);
}
return user;
}
static const gchar*
cockpit_ssh_connect (CockpitSshData *data,
const gchar *host_arg,
ssh_channel *out_channel)
{
const gchar *ignore_hostkey;
const gchar *problem;
const gchar *knownhosts;
guint port = 22;
gchar *host;
ssh_channel channel;
int rc;
parse_host (host_arg, &host, &data->username, &port);
/* Username always comes from auth message when using basic */
if (g_strcmp0 (data->auth_type, "basic") == 0)
{
g_free (data->username);
data->username = username_from_basic (data->initial_auth_data);
}
if (!data->username || *data->username == '\0')
{
g_message ("%s: No username provided", data->logname);
problem = "authentication-failed";
goto out;
}
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_USER, data->username) == 0);
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_PORT, &port) == 0);
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_HOST, host) == 0);
if (!data->ssh_options->ignore_hostkey)
{
/* This is a single host, for which we have been told to ignore the host key */
ignore_hostkey = cockpit_conf_string (COCKPIT_CONF_SSH_SECTION, "host");
if (!ignore_hostkey)
ignore_hostkey = "127.0.0.1";
data->ssh_options->ignore_hostkey = g_str_equal (ignore_hostkey, host);
}
if (!data->ssh_options->ignore_hostkey)
{
problem = set_knownhosts_file (data, host, port);
if (problem != NULL)
goto out;
knownhosts = data->ssh_options->knownhosts_file;
}
else
{
knownhosts = "/dev/null";
}
// Set knownhosts before trying to connect to ensure key exchange uses the right file
g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_KNOWNHOSTS, knownhosts) == 0);
rc = ssh_connect (data->session);
if (rc != SSH_OK)
{
g_message ("%s: %d couldn't connect: %s '%s' '%d'", data->logname, rc,
ssh_get_error (data->session), host, port);
problem = "no-host";
goto out;
}
g_debug ("%s: connected", data->logname);
if (!data->ssh_options->ignore_hostkey)
{
problem = verify_knownhost (data, host, port);
if (problem != NULL)
goto out;
}
/* The problem returned when auth failure */
problem = cockpit_ssh_authenticate (data);
if (problem != NULL)
goto out;
channel = ssh_channel_new (data->session);
rc = ssh_channel_open_session (channel);
if (rc != SSH_OK)
{
g_message ("%s: couldn't open session: %s", data->logname,
ssh_get_error (data->session));
problem = "internal-error";
goto out;
}
if (data->ssh_options->remote_peer)
{
/* Try to set the remote peer env var, this will
* often fail as ssh servers have to be configured
* to allow it.
*/
rc = ssh_channel_request_env (channel, "COCKPIT_REMOTE_PEER",
data->ssh_options->remote_peer);
if (rc != SSH_OK)
{
g_debug ("%s: Couldn't set COCKPIT_REMOTE_PEER: %s",
data->logname,
ssh_get_error (data->session));
}
}
g_debug ("%s: opened channel", data->logname);
*out_channel = channel;
out:
g_free (host);
return problem;
}
static void
cockpit_ssh_data_free (CockpitSshData *data)
{
if (data->initial_auth_data)
{
memset (data->initial_auth_data, 0, strlen (data->initial_auth_data));
free (data->initial_auth_data);
}
g_free (data->host_key);
if (data->host_fingerprint)
ssh_string_free_char (data->host_fingerprint);
if (data->auth_results)
g_hash_table_destroy (data->auth_results);
g_free (data->conversation);
g_free (data->username);
g_free (data->ssh_options);
g_free (data->user_known_hosts);
g_free (data->auth_type);
g_strfreev (data->env);
g_free (data);
}
#define COCKPIT_SSH_RELAY(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_SSH_RELAY, CockpitSshRelay))
struct _CockpitSshRelay {
GObject parent_instance;
CockpitSshData *ssh_data;
gboolean sent_disconnect;
gboolean received_eof;
gboolean received_frame;
gboolean received_close;
gboolean received_exit;
gboolean sent_close;
gboolean sent_eof;
guint exit_code;
guint sig_read;
guint sig_close;
gboolean pipe_closed;
CockpitPipe *pipe;
GQueue *queue;
gsize partial;
gchar *logname;
gchar *connection_string;
ssh_session session;
ssh_channel channel;
ssh_event event;
GSource *io;
struct ssh_channel_callbacks_struct channel_cbs;
};
struct _CockpitSshRelayClass {
GObjectClass parent_class;
};
static guint sig_disconnect = 0;
enum {
PROP_0,
PROP_CONNECTION_STRING
};
G_DEFINE_TYPE (CockpitSshRelay, cockpit_ssh_relay, G_TYPE_OBJECT);
static void
cockpit_ssh_relay_dispose (GObject *object)
{
CockpitSshRelay *self = COCKPIT_SSH_RELAY (object);
g_assert (self->ssh_data == NULL);
if (self->sig_read > 0)
g_signal_handler_disconnect (self->pipe, self->sig_read);
self->sig_read = 0;
if (self->sig_close > 0)
g_signal_handler_disconnect (self->pipe, self->sig_close);
self->sig_close = 0;
if (self->io)
g_source_destroy (self->io);
G_OBJECT_CLASS (cockpit_ssh_relay_parent_class)->dispose (object);
}
static void
cockpit_ssh_relay_finalize (GObject *object)
{
CockpitSshRelay *self = COCKPIT_SSH_RELAY (object);
if (self->pipe)
g_object_unref (self->pipe);
g_queue_free_full (self->queue, (GDestroyNotify)g_bytes_unref);
if (self->event)
ssh_event_free (self->event);
/* libssh channels like to hang around even after they're freed */
if (self->channel)
memset (&self->channel_cbs, 0, sizeof (self->channel_cbs));
g_free (self->logname);
g_free (self->connection_string);
if (self->io)
g_source_unref (self->io);
ssh_disconnect (self->session);
ssh_free (self->session);
G_OBJECT_CLASS (cockpit_ssh_relay_parent_class)->finalize (object);
}
static gboolean
emit_disconnect (gpointer user_data)
{
CockpitSshRelay *self = user_data;
if (!self->sent_disconnect)
{
self->sent_disconnect = TRUE;
g_signal_emit (self, sig_disconnect, 0);
}
return FALSE;
}
static void
cockpit_relay_disconnect (CockpitSshRelay *self,
const gchar *problem)
{
if (self->ssh_data)
{
send_auth_reply (self->ssh_data, problem ? problem : exit_code_problem (self->exit_code));
cockpit_ssh_data_free (self->ssh_data);
self->ssh_data = NULL;
}
/* libssh channels like to hang around even after they're freed */
if (self->channel)
memset (&self->channel_cbs, 0, sizeof (self->channel_cbs));
self->channel = NULL;
if (self->io)
g_source_destroy (self->io);
g_timeout_add (0, emit_disconnect, self);
}
static int
on_channel_data (ssh_session session,
ssh_channel channel,
void *data,
uint32_t len,
int is_stderr,
void *userdata)
{
CockpitSshRelay *self = userdata;
guint32 size, i;
gint ret = 0;
GBytes *bytes = NULL;
guint8 *bdata = data;
if (!self->received_frame && !is_stderr)
{
size = 0;
for (i = 0; i < len; i++)
{
/* Check invalid characters, prevent integer overflow, limit max length */
if (i > 7 || bdata[i] < '0' || bdata[i] > '9')
break;
size *= 10;
size += bdata[i] - '0';
}
/* If we don't have enough data return 0 bytes processed
* so that this data will be included in the next callback
*/
if (i == len)
goto out;
/*
* So we may be talking to a process that's not cockpit-bridge. How does
* that happen? ssh always executes commands inside of a shell ... and
* bash prints its 'cockpit-bridge: not found' message on stdout (!)
*
* So we degrade gracefully in this case, and start to treat output as
* error output.
*/
if (bdata[i] != '\n')
{
self->exit_code = NO_COCKPIT;
}
else
{
self->received_frame = TRUE;
cockpit_ssh_data_free (self->ssh_data);
self->ssh_data = NULL;
}
}
if (is_stderr || self->exit_code == NO_COCKPIT)
{
g_printerr ("%s", bdata);
ret = len;
}
else if (self->received_frame)
{
if (!self->pipe_closed)
{
bytes = g_bytes_new (bdata, len);
cockpit_pipe_write (self->pipe, bytes);
g_bytes_unref (bytes);
ret = len;
}
else
{
g_debug ("%s: dropping %d incoming bytes, pipe is closed", self->logname, len);
ret = len;
}
}
out:
return ret;
}
static void
on_channel_eof (ssh_session session,
ssh_channel channel,
void *userdata)
{
CockpitSshRelay *self = userdata;
g_debug ("%s: received eof", self->logname);
self->received_eof = TRUE;
}
static void
on_channel_close (ssh_session session,
ssh_channel channel,
void *userdata)
{
CockpitSshRelay *self = userdata;
g_debug ("%s: received close", self->logname);
self->received_close = TRUE;
}
static void
on_channel_exit_signal (ssh_session session,
ssh_channel channel,
const char *signal,
int core,
const char *errmsg,
const char *lang,
void *userdata)
{
CockpitSshRelay *self = userdata;
guint exit_code;
g_return_if_fail (signal != NULL);
self->received_exit = TRUE;
if (g_ascii_strcasecmp (signal, "TERM") == 0 ||
g_ascii_strcasecmp (signal, "Terminated") == 0)
{
g_debug ("%s: received TERM signal", self->logname);
exit_code = TERMINATED;
}
else
{
g_warning ("%s: bridge killed%s%s%s%s", self->logname,
signal ? " by signal " : "", signal ? signal : "",
errmsg && errmsg[0] ? ": " : "", errmsg ? errmsg : "");
exit_code = INTERNAL_ERROR;
}
if (!self->exit_code)
self->exit_code = exit_code;
cockpit_relay_disconnect (self, NULL);
}
static void
on_channel_signal (ssh_session session,
ssh_channel channel,
const char *signal,
void *userdata)
{
/*
* HACK: So it looks like libssh is buggy and is confused about
* the difference between "exit-signal" and "signal" in section 6.10
* of the RFC. Accept signal as a usable substitute
*/
if (g_ascii_strcasecmp (signal, "TERM") == 0 ||
g_ascii_strcasecmp (signal, "Terminated") == 0)
on_channel_exit_signal (session, channel, signal, 0, NULL, NULL, userdata);
}
static void
on_channel_exit_status (ssh_session session,
ssh_channel channel,
int exit_status,
void *userdata)
{
CockpitSshRelay *self = userdata;
guint exit_code = 0;
self->received_exit = TRUE;
if (exit_status == 127)
{
g_debug ("%s: received exit status %d", self->logname, exit_status);
exit_code = NO_COCKPIT; /* cockpit-bridge not installed */
}
else if (!self->received_frame)
{
g_message ("%s: spawning remote bridge failed with %d status", self->logname, exit_status);
exit_code = NO_COCKPIT;
}
else if (exit_status)
{
g_message ("%s: remote bridge exited with %d status", self->logname, exit_status);
exit_code = INTERNAL_ERROR;
}
if (!self->exit_code && exit_code)
self->exit_code = exit_code;
cockpit_relay_disconnect (self, NULL);
}
static gboolean
dispatch_queue (CockpitSshRelay *self)
{
GBytes *block;
const guchar *data;
const gchar *msg;
gsize length;
gsize want;
int rc;
if (self->sent_eof)
return FALSE;
if (self->received_close)
return FALSE;
for (;;)
{
block = g_queue_peek_head (self->queue);
if (!block)
return FALSE;
data = g_bytes_get_data (block, &length);
g_assert (self->partial <= length);
want = length - self->partial;
rc = ssh_channel_write (self->channel, data + self->partial, want);
if (rc < 0)
{
msg = ssh_get_error (self->session);
if (ssh_get_error_code (self->session) == SSH_REQUEST_DENIED)
{
g_debug ("%s: couldn't write: %s", self->logname, msg);
return FALSE;
}
else if (ssh_msg_is_disconnected (msg))
{
g_message ("%s: couldn't write: %s", self->logname, msg);
self->received_close = TRUE;
self->received_eof = TRUE;
return FALSE;
}
else
{
g_warning ("%s: couldn't write: %s", self->logname, msg);
return FALSE;
}
break;
}
if (rc == want)
{
g_debug ("%s: wrote %d bytes", self->logname, rc);
g_queue_pop_head (self->queue);
g_bytes_unref (block);
self->partial = 0;
}
else
{
g_debug ("%s: wrote %d of %d bytes", self->logname, rc, (int)want);
g_return_val_if_fail (rc < want, FALSE);
self->partial += rc;
if (rc == 0)
break;
}
}
return TRUE;
}
static void
dispatch_close (CockpitSshRelay *self)
{
g_assert (!self->sent_close);
switch (ssh_channel_close (self->channel))
{
case SSH_AGAIN:
g_debug ("%s: will send close later", self->logname);
break;
case SSH_OK:
g_debug ("%s: sent close", self->logname);
self->sent_close = TRUE;
break;
default:
if (ssh_get_error_code (self->session) == SSH_REQUEST_DENIED)
{
g_debug ("%s: couldn't send close: %s", self->logname,
ssh_get_error (self->session));
self->sent_close = TRUE; /* channel is already closed */
}
else
{
g_warning ("%s: couldn't send close: %s", self->logname,
ssh_get_error (self->session));
self->received_exit = TRUE;
if (!self->exit_code)
self->exit_code = INTERNAL_ERROR;
cockpit_relay_disconnect (self, NULL);
}
break;
}
}
static void
dispatch_eof (CockpitSshRelay *self)
{
g_assert (!self->sent_eof);
switch (ssh_channel_send_eof (self->channel))
{
case SSH_AGAIN:
g_debug ("%s: will send eof later", self->logname);
break;
case SSH_OK:
g_debug ("%s: sent eof", self->logname);
self->sent_eof = TRUE;
break;
default:
if (ssh_get_error_code (self->session) == SSH_REQUEST_DENIED)
{
g_debug ("%s: couldn't send eof: %s", self->logname,
ssh_get_error (self->session));
self->sent_eof = TRUE; /* channel is already closed */
}
else
{
g_warning ("%s: couldn't send eof: %s", self->logname,
ssh_get_error (self->session));
self->received_exit = TRUE;
if (!self->exit_code)
self->exit_code = INTERNAL_ERROR;
cockpit_relay_disconnect (self, NULL);
}
break;
}
}
static void
on_pipe_read (CockpitPipe *pipe,
GByteArray *input,
gboolean end_of_data,
gpointer user_data)
{
CockpitSshRelay *self = user_data;
GByteArray *buf = NULL;
buf = cockpit_pipe_get_buffer (pipe);
g_byte_array_ref (buf);
if (!self->sent_eof && !self->received_close && buf->len > 0)
{
g_debug ("%s: queued %d bytes", self->logname, buf->len);
g_queue_push_tail (self->queue, g_byte_array_free_to_bytes (buf));
}
else
{
g_debug ("%s: dropping %d bytes", self->logname, buf->len);
g_byte_array_free (buf, TRUE);
}
}
static void
on_pipe_close (CockpitPipe *pipe,
const gchar *problem,
gpointer user_data)
{
CockpitSshRelay *self = user_data;
self->pipe_closed = TRUE;
// Pipe closing before data was received doesn't mean no-cockpit
self->received_frame = TRUE;
if (!self->received_eof)
dispatch_eof (self);
}
typedef struct {
GSource source;
GPollFD pfd;
CockpitSshRelay *relay;
} CockpitSshSource;
static gboolean
cockpit_ssh_source_check (GSource *source)
{
CockpitSshSource *cs = (CockpitSshSource *)source;
return (cs->pfd.events & cs->pfd.revents) != 0;
}
static gboolean
cockpit_ssh_source_prepare (GSource *source,
gint *timeout)
{
CockpitSshSource *cs = (CockpitSshSource *)source;
CockpitSshRelay *self = cs->relay;
gint status;
*timeout = 1;
status = ssh_get_status (self->session);
cs->pfd.revents = 0;
cs->pfd.events = G_IO_IN | G_IO_ERR | G_IO_NVAL | G_IO_HUP;
/* libssh has something in its buffer: want to write */
if (status & SSH_WRITE_PENDING)
cs->pfd.events |= G_IO_OUT;
/* We have something in our queue: want to write */
else if (!g_queue_is_empty (self->queue))
cs->pfd.events |= G_IO_OUT;
/* We are closing and need to send eof: want to write */
else if (self->pipe_closed && !self->sent_eof)
cs->pfd.events |= G_IO_OUT;
/* Need to reply to an EOF or close */
if ((self->received_eof && self->sent_eof && !self->sent_close) ||
(self->received_close && !self->sent_close))
cs->pfd.events |= G_IO_OUT;
return cockpit_ssh_source_check (source);
}
static gboolean
cockpit_ssh_source_dispatch (GSource *source,
GSourceFunc callback,
gpointer user_data)
{
CockpitSshSource *cs = (CockpitSshSource *)source;
int rc;
const gchar *msg;
gboolean ret = TRUE;
CockpitSshRelay *self = cs->relay;
GIOCondition cond = cs->pfd.revents;
if (cond & (G_IO_HUP | G_IO_ERR))
{
if (self->sent_close || self->sent_eof)
{
self->received_eof = TRUE;
self->received_close = TRUE;
}
}
if (self->received_exit)
return FALSE;
g_return_val_if_fail ((cond & G_IO_NVAL) == 0, FALSE);
/*
* HACK: Yes this is anohter poll() call. The async support in
* libssh is quite hacky right now.
*
* https://red.libssh.org/issues/155
*/
rc = ssh_event_dopoll (self->event, 0);
switch (rc)
{
case SSH_OK:
case SSH_AGAIN:
break;
case SSH_ERROR:
msg = ssh_get_error (self->session);
/*
* HACK: There doesn't seem to be a way to get at the original socket errno
* here. So we have to screen scrape.
*
* https://red.libssh.org/issues/158
*/
if (ssh_msg_is_disconnected (msg))
{
g_debug ("%s: failed to process channel: %s", self->logname, msg);
self->received_exit = TRUE;
if (!self->exit_code)
self->exit_code = TERMINATED;
}
else
{
g_message ("%s: failed to process channel: %s", self->logname, msg);
self->received_exit = TRUE;
if (!self->exit_code)
self->exit_code = INTERNAL_ERROR;
}
ret = FALSE;
break;
default:
self->received_exit = TRUE;
if (!self->exit_code)
self->exit_code = INTERNAL_ERROR;
g_critical ("%s: ssh_event_dopoll() returned %d", self->logname, rc);
ret = FALSE;
}
if (!ret)
goto out;
if (cond & G_IO_ERR)
{
g_message ("%s: error reading from ssh", self->logname);
ret = FALSE;
self->received_exit = TRUE;
if (!self->exit_code)
self->exit_code = DISCONNECTED;
goto out;
}
if (cond & G_IO_OUT)
{
if (!dispatch_queue (self) && self->pipe_closed && !self->sent_eof)
dispatch_eof (self);
if (self->received_eof && self->sent_eof && !self->sent_close)
dispatch_close (self);
if (self->received_eof && !self->received_close && !self->sent_close)
dispatch_close (self);
}
out:
if (self->received_exit)
cockpit_relay_disconnect (self, NULL);
return ret;
}
static GSource *
cockpit_ssh_relay_start_source (CockpitSshRelay *self) {
static GSourceFuncs source_funcs = {
cockpit_ssh_source_prepare,
cockpit_ssh_source_check,
cockpit_ssh_source_dispatch,
NULL,
};
GSource *source = g_source_new (&source_funcs, sizeof (CockpitSshSource));
CockpitSshSource *cs = (CockpitSshSource *)source;
cs->relay = self;
cs->pfd.fd = ssh_get_fd (self->session);
g_source_add_poll (source, &cs->pfd);
g_source_attach (source, g_main_context_default ());
return source;
}
static void
cockpit_ssh_relay_start (CockpitSshRelay *self,
gint outfd)
{
const gchar *problem;
int rc;
static struct ssh_channel_callbacks_struct channel_cbs = {
.channel_data_function = on_channel_data,
.channel_eof_function = on_channel_eof,
.channel_close_function = on_channel_close,
.channel_signal_function = on_channel_signal,
.channel_exit_signal_function = on_channel_exit_signal,
.channel_exit_status_function = on_channel_exit_status,
};
self->ssh_data->outfd = outfd;
self->ssh_data->initial_auth_data = challenge_for_auth_data ("*", outfd,
&self->ssh_data->auth_type);
problem = cockpit_ssh_connect (self->ssh_data, self->connection_string, &self->channel);
if (problem)
goto out;
self->event = ssh_event_new ();
memcpy (&self->channel_cbs, &channel_cbs, sizeof (channel_cbs));
self->channel_cbs.userdata = self;
ssh_callbacks_init (&self->channel_cbs);
ssh_set_channel_callbacks (self->channel, &self->channel_cbs);
ssh_set_blocking (self->session, 0);
ssh_event_add_session (self->event, self->session);
self->pipe = g_object_new (COCKPIT_TYPE_PIPE,
"in-fd", 0,
"out-fd", outfd,
"name", self->logname,
NULL);
self->sig_read = g_signal_connect (self->pipe,
"read",
G_CALLBACK (on_pipe_read),
self);
self->sig_close = g_signal_connect (self->pipe,
"close",
G_CALLBACK (on_pipe_close),
self);
for (rc = SSH_AGAIN; rc == SSH_AGAIN; )
rc = ssh_channel_request_exec (self->channel, self->ssh_data->ssh_options->command);
if (rc != SSH_OK)
{
g_message ("%s: couldn't execute command: %s: %s", self->logname,
self->ssh_data->ssh_options->command,
ssh_get_error (self->session));
problem = "internal-error";
goto out;
}
self->io = cockpit_ssh_relay_start_source (self);
out:
if (problem)
{
self->exit_code = AUTHENTICATION_FAILED;
cockpit_relay_disconnect (self, problem);
close (outfd);
}
}
static void
cockpit_ssh_relay_init (CockpitSshRelay *self)
{
const gchar *debug;
ssh_init ();
self->queue = g_queue_new ();
debug = g_getenv ("G_MESSAGES_DEBUG");
if (debug && (strstr (debug, "libssh") || g_strcmp0 (debug, "all") == 0))
ssh_set_log_level (SSH_LOG_FUNCTIONS);
}
static void
cockpit_ssh_relay_set_property (GObject *obj,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
CockpitSshRelay *self = COCKPIT_SSH_RELAY (obj);
switch (prop_id)
{
case PROP_CONNECTION_STRING:
self->connection_string = g_value_dup_string (value);
self->logname = g_strdup_printf ("cockpit-ssh %s", self->connection_string);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
break;
}
}
static void
cockpit_ssh_relay_constructed (GObject *object)
{
CockpitSshRelay *self = COCKPIT_SSH_RELAY (object);
G_OBJECT_CLASS (cockpit_ssh_relay_parent_class)->constructed (object);
self->session = ssh_new ();
self->ssh_data = g_new0 (CockpitSshData, 1);
self->ssh_data->env = g_get_environ ();
self->ssh_data->session = self->session;
self->ssh_data->logname = self->logname;
self->ssh_data->auth_results = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
self->ssh_data->ssh_options = cockpit_ssh_options_from_env (self->ssh_data->env);
self->ssh_data->user_known_hosts = g_build_filename (g_get_home_dir (), ".ssh/known_hosts", NULL);
}
static void
authorize_logger (const char *data)
{
g_message ("%s", data);
}
static void
cockpit_ssh_relay_class_init (CockpitSshRelayClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = cockpit_ssh_relay_dispose;
object_class->finalize = cockpit_ssh_relay_finalize;
object_class->constructed = cockpit_ssh_relay_constructed;
object_class->set_property = cockpit_ssh_relay_set_property;
g_object_class_install_property (object_class, PROP_CONNECTION_STRING,
g_param_spec_string ("connection-string", NULL, NULL, "localhost",
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
sig_disconnect = g_signal_new ("disconnect", COCKPIT_TYPE_SSH_RELAY,
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
cockpit_authorize_logger (authorize_logger, 0);
}
CockpitSshRelay *
cockpit_ssh_relay_new (const gchar *connection_string,
gint outfd)
{
CockpitSshRelay *self = g_object_new (COCKPIT_TYPE_SSH_RELAY,
"connection-string", connection_string,
NULL);
cockpit_ssh_relay_start (self, outfd);
return self;
}
gint
cockpit_ssh_relay_result (CockpitSshRelay* self)
{
return self->exit_code;
}