/*
* This file is part of Cockpit.
*
* Copyright (C) 2013 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 "cockpitauth.h"
#include "cockpitws.h"
#include "websocket/websocket.h"
#include "common/cockpitauthorize.h"
#include "common/cockpitconf.h"
#include "common/cockpiterror.h"
#include "common/cockpithex.h"
#include "common/cockpitlog.h"
#include "common/cockpitjson.h"
#include "common/cockpitmemory.h"
#include "common/cockpitpipe.h"
#include "common/cockpitpipetransport.h"
#include "common/cockpitsystem.h"
#include "common/cockpitunixfd.h"
#include "common/cockpitwebserver.h"
#include
#include
#include
#include
#include
#include
#define ACTION_SSH "remote-login-ssh"
#define ACTION_NONE "none"
/* Some tunables that can be set from tests */
const gchar *cockpit_ws_session_program =
PACKAGE_LIBEXEC_DIR "/cockpit-session";
const gchar *cockpit_ws_ssh_program =
PACKAGE_LIBEXEC_DIR "/cockpit-ssh";
/* Timeout of authenticated session when no connections */
guint cockpit_ws_service_idle = 15;
/* Timeout of everything when noone is connected */
guint cockpit_ws_process_idle = 90;
/* The amount of time a spawned process has to complete authentication */
guint cockpit_ws_auth_process_timeout = 30;
guint cockpit_ws_auth_response_timeout = 60;
/* Maximum number of pending authentication requests */
const gchar *cockpit_ws_max_startups = NULL;
static guint max_startups = 10;
static guint sig__idling = 0;
/* Tristate tracking whether gssapi works properly */
static gint gssapi_available = -1;
G_DEFINE_TYPE (CockpitAuth, cockpit_auth, G_TYPE_OBJECT)
typedef struct {
gint refs;
gchar *name;
gchar *cookie;
CockpitAuth *auth;
CockpitWebService *service;
gboolean initialized;
guint timeout_tag;
gulong idling_sig;
gulong destroy_sig;
/* Used during authentication */
CockpitTransport *transport;
gulong control_sig;
gulong close_sig;
guint client_timeout;
guint authorize_timeout;
/* An open /login request from client */
GSimpleAsyncResult *result;
/* An authorization header from client */
gchar *authorization;
/* An authorize challenge from session */
JsonObject *authorize;
/* The conversation in progress */
gchar *conversation;
} CockpitSession;
static void
cockpit_session_reset (gpointer data)
{
CockpitSession *session = data;
GSimpleAsyncResult *result;
char *conversation;
CockpitAuth *self;
char *cookie;
if (session->result)
{
result = session->result;
session->result = NULL;
g_simple_async_result_complete (result);
g_object_unref (result);
}
if (session->authorization)
{
cockpit_memory_clear (session->authorization, -1);
g_free (session->authorization);
session->authorization = NULL;
}
conversation = session->conversation;
session->conversation = NULL;
cookie = session->cookie;
session->cookie = NULL;
self = session->auth;
/* No accessing session after this point */
if (cookie)
{
g_hash_table_remove (self->sessions, cookie);
g_free (cookie);
}
if (conversation)
{
g_hash_table_remove (self->conversations, conversation);
g_free (conversation);
}
}
static CockpitSession *
cockpit_session_ref (CockpitSession *session)
{
session->refs++;
return session;
}
static void
on_web_service_gone (gpointer data,
GObject *where_the_object_was)
{
CockpitSession *session = data;
session->service = NULL;
cockpit_session_reset (session);
}
static void
cockpit_session_unref (gpointer data)
{
CockpitSession *session = data;
CockpitCreds *creds;
GObject *object;
session->refs--;
if (session->refs > 0)
return;
cockpit_session_reset (data);
g_free (session->name);
g_free (session->cookie);
if (session->authorize)
json_object_unref (session->authorize);
if (session->transport)
{
if (session->control_sig)
g_signal_handler_disconnect (session->transport, session->control_sig);
if (session->close_sig)
g_signal_handler_disconnect (session->transport, session->close_sig);
g_object_unref (session->transport);
}
if (session->service)
{
creds = cockpit_web_service_get_creds (session->service);
object = G_OBJECT (session->service);
session->service = NULL;
if (creds)
cockpit_creds_poison (creds);
if (session->idling_sig)
g_signal_handler_disconnect (object, session->idling_sig);
if (session->destroy_sig)
g_signal_handler_disconnect (object, session->destroy_sig);
g_object_weak_unref (object, on_web_service_gone, session);
g_object_run_dispose (object);
g_object_unref (object);
}
if (session->timeout_tag)
g_source_remove (session->timeout_tag);
g_free (session);
}
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 void
cockpit_auth_finalize (GObject *object)
{
CockpitAuth *self = COCKPIT_AUTH (object);
if (self->timeout_tag)
g_source_remove (self->timeout_tag);
g_bytes_unref (self->key);
g_hash_table_remove_all (self->sessions);
g_hash_table_remove_all (self->conversations);
g_hash_table_destroy (self->sessions);
g_hash_table_destroy (self->conversations);
G_OBJECT_CLASS (cockpit_auth_parent_class)->finalize (object);
}
static gboolean
on_process_timeout (gpointer data)
{
CockpitAuth *self = COCKPIT_AUTH (data);
self->timeout_tag = 0;
if (g_hash_table_size (self->sessions) == 0)
{
g_debug ("auth is idle");
g_signal_emit (self, sig__idling, 0);
}
return FALSE;
}
static void
cockpit_auth_init (CockpitAuth *self)
{
static const gsize key_len = 128;
gpointer key;
key = cockpit_authorize_nonce (key_len);
if (!key)
g_error ("couldn't read random key, startup aborted");
self->key = g_bytes_new_take (key, key_len);
self->sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
NULL, cockpit_session_unref);
self->conversations = g_hash_table_new_full (g_str_hash, g_str_equal,
NULL, cockpit_session_unref);
self->timeout_tag = g_timeout_add_seconds (cockpit_ws_process_idle,
on_process_timeout, self);
self->startups = 0;
self->max_startups = max_startups;
self->max_startups_begin = max_startups;
self->max_startups_rate = 100;
}
gchar *
cockpit_auth_nonce (CockpitAuth *self)
{
const guchar *key;
gsize len;
guint64 seed;
seed = self->nonce_seed++;
key = g_bytes_get_data (self->key, &len);
return g_compute_hmac_for_data (G_CHECKSUM_SHA256, key, len,
(guchar *)&seed, sizeof (seed));
}
static gchar *
get_remote_address (GIOStream *io)
{
GSocketAddress *remote = NULL;
GSocketConnection *connection = NULL;
GIOStream *base;
gchar *result = NULL;
if (G_IS_TLS_CONNECTION (io))
{
g_object_get (io, "base-io-stream", &base, NULL);
if (G_IS_SOCKET_CONNECTION (base))
connection = g_object_ref (base);
g_object_unref (base);
}
else if (G_IS_SOCKET_CONNECTION (io))
{
connection = g_object_ref (io);
}
if (connection)
remote = g_socket_connection_get_remote_address (connection, NULL);
if (remote && G_IS_INET_SOCKET_ADDRESS (remote))
result = g_inet_address_to_string (g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (remote)));
if (remote)
g_object_unref (remote);
if (connection)
g_object_unref (connection);
return result;
}
gchar *
cockpit_auth_steal_authorization (GHashTable *headers,
GIOStream *connection,
gchar **ret_type,
gchar **ret_conversation)
{
char *type = NULL;
gchar *ret = NULL;
gchar *line;
gpointer key;
g_assert (headers != NULL);
g_assert (ret_conversation != NULL);
g_assert (ret_type != NULL);
/* Avoid copying as it can contain passwords */
if (g_hash_table_lookup_extended (headers, "Authorization", &key, (gpointer *)&line))
{
g_hash_table_steal (headers, "Authorization");
g_free (key);
}
else
{
/*
* If we don't yet know that Negotiate authentication is possible
* or not, then we ask our session to try to do Negotiate auth
* but without any input data.
*/
if (gssapi_available != 0)
line = g_strdup ("Negotiate");
else
return NULL;
}
/* Dig out the authorization type */
if (!cockpit_authorize_type (line, &type))
goto out;
/* If this is a conversation, get that part out too */
if (g_str_equal (type, "x-conversation"))
{
if (!cockpit_authorize_subject (line, ret_conversation))
goto out;
}
/*
* So for negotiate authentication, conversation happens on a
* single connection. Yes that's right, GSSAPI, NTLM, and all
* those nice mechanisms are keep-alive based, not HTTP request based.
*/
else if (g_str_equal (type, "negotiate"))
{
/* Resume an already running conversation? */
if (ret_conversation && connection)
*ret_conversation = g_strdup (g_object_get_data (G_OBJECT (connection), type));
}
if (ret_type)
{
*ret_type = type;
type = NULL;
}
ret = line;
line = NULL;
out:
g_free (line);
g_free (type);
return ret;
}
static const gchar *
type_option (const gchar *type,
const gchar *option,
const gchar *default_str)
{
if (type && cockpit_conf_string (type, option))
return cockpit_conf_string (type, option);
return default_str;
}
static guint
timeout_option (const gchar *name,
const gchar *type,
guint default_value)
{
return cockpit_conf_guint (type, name, default_value,
MAX_AUTH_TIMEOUT, MIN_AUTH_TIMEOUT);
}
static const gchar *
application_parse_host (const gchar *application)
{
const gchar *prefix = "cockpit+=";
gint len = strlen (prefix);
g_return_val_if_fail (application != NULL, NULL);
if (g_str_has_prefix (application, prefix) && application[len] != '\0')
return application + len;
else
return NULL;
}
static gchar *
application_cookie_name (const gchar *application)
{
const gchar *host = application_parse_host (application);
gchar *cookie_name = NULL;
if (host)
cookie_name = g_strdup_printf ("machine-cockpit+%s", host);
else
cookie_name = g_strdup (application);
return cookie_name;
}
/* Struct for adding tty later */
typedef struct {
int io;
} ChildData;
static void
session_child_setup (gpointer data)
{
ChildData *child = data;
if (dup2 (child->io, 0) < 0 || dup2 (child->io, 1) < 0)
{
g_printerr ("couldn't set child stdin/stout file descriptors\n");
_exit (127);
}
close (child->io);
if (cockpit_unix_fd_close_all (3, -1) < 0)
{
g_printerr ("couldn't close file descriptors: %m\n");
_exit (127);
}
}
static CockpitTransport *
session_start_process (const gchar **argv,
const gchar **env)
{
CockpitTransport *transport = NULL;
CockpitPipe *pipe = NULL;
GError *error = NULL;
ChildData child;
gboolean ret;
GPid pid = 0;
int fds[2];
g_debug ("spawning %s", argv[0]);
/* The main stdin/stdout for the socket ... both are read/writable */
if (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds) < 0)
{
g_warning ("couldn't create loopback socket: %s", g_strerror (errno));
return NULL;
}
child.io = fds[0];
ret = g_spawn_async_with_pipes (NULL, (gchar **)argv, (gchar **)env,
G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_LEAVE_DESCRIPTORS_OPEN,
session_child_setup, &child,
&pid, NULL, NULL, NULL, &error);
close (fds[0]);
if (!ret)
{
g_message ("couldn't launch cockpit session: %s: %s", argv[0], error->message);
g_error_free (error);
close (fds[1]);
return NULL;
}
pipe = g_object_new (COCKPIT_TYPE_PIPE,
"in-fd", fds[1],
"out-fd", fds[1],
"pid", pid,
"name", argv[0],
NULL);
transport = cockpit_pipe_transport_new (pipe);
g_object_unref (pipe);
return transport;
}
static void
send_authorize_reply (CockpitTransport *transport,
const gchar *cookie,
const gchar *authorization)
{
const gchar *fields[] = {
"command", "authorize",
"cookie", cookie,
"response", authorization,
NULL
};
GBytes *payload;
const gchar *delim;
GByteArray *buffer;
JsonNode *node;
gchar *encoded;
gsize length;
guint i;
buffer = g_byte_array_new ();
for (i = 0; fields[i] != NULL; i++)
{
if (i % 2 == 0)
delim = buffer->len == 0 ? "{" : ",";
else
delim = ":";
g_byte_array_append (buffer, (guchar *)delim, 1);
node = json_node_init_string (json_node_new (JSON_NODE_VALUE), fields[i]);
encoded = cockpit_json_write (node, &length);
g_byte_array_append (buffer, (guchar *)encoded, length);
cockpit_memory_clear ((guchar *)json_node_get_string (node), -1);
json_node_free (node);
g_free (encoded);
}
g_byte_array_append (buffer, (guchar *)"}", 1);
payload = g_bytes_new_with_free_func (buffer->data, buffer->len,
byte_array_clear_and_free, buffer);
cockpit_transport_send (transport, NULL, payload);
g_bytes_unref (payload);
}
static gboolean
reply_authorize_challenge (CockpitSession *session)
{
const gchar *challenge = NULL;
char *authorize_type = NULL;
char *authorization_type = NULL;
const gchar *cookie = NULL;
const gchar *response = NULL;
JsonObject *login_data = NULL;
gboolean ret = FALSE;
if (!session->authorize)
goto out;
if (!cockpit_json_get_string (session->authorize, "cookie", NULL, &cookie) ||
!cockpit_json_get_string (session->authorize, "challenge", NULL, &challenge) ||
!cockpit_json_get_string (session->authorize, "response", NULL, &response))
goto out;
if (response && !cookie)
{
cockpit_memory_clear (session->authorization, -1);
g_free (session->authorization);
session->authorization = g_strdup (response);
ret = TRUE;
goto out;
}
if (!challenge || !cookie)
goto out;
if (!cockpit_authorize_type (challenge, &authorize_type))
goto out;
/* Handle prompting for login data */
if (g_str_equal (authorize_type, "x-login-data"))
{
if (cockpit_json_get_object (session->authorize, "login-data", NULL, &login_data) && login_data)
cockpit_creds_set_login_data (cockpit_web_service_get_creds (session->service), login_data);
ret = TRUE;
goto out;
}
if (!session->authorization)
goto out;
if (cockpit_authorize_type (session->authorization, &authorization_type) &&
(g_str_equal (authorize_type, "*") || g_str_equal (authorize_type, authorization_type)))
{
send_authorize_reply (session->transport, cookie, session->authorization);
cockpit_memory_clear (session->authorization, -1);
g_free (session->authorization);
session->authorization = NULL;
ret = TRUE;
}
out:
free (authorize_type);
free (authorization_type);
return ret;
}
static gboolean
on_authorize_timeout (gpointer data)
{
CockpitSession *session = data;
CockpitTransport *transport = cockpit_web_service_get_transport (session->service);
session->timeout_tag = 0;
g_message ("%s: session timed out during authentication", session->name);
cockpit_transport_close (transport, "timeout");
return FALSE;
}
static void
reset_authorize_timeout (CockpitSession *session,
gboolean waiting_for_client)
{
guint seconds = waiting_for_client ? session->client_timeout : session->authorize_timeout;
if (session->timeout_tag)
g_source_remove (session->timeout_tag);
session->timeout_tag = g_timeout_add_seconds (seconds, on_authorize_timeout, session);
}
static void
propagate_problem_to_error (CockpitSession *session,
JsonObject *options,
const gchar *problem,
const gchar *message,
GError **error)
{
char *type = NULL;
const char *pw_result = NULL;
JsonObject *auth_results = NULL;
g_return_if_fail (error != NULL);
if (g_str_equal (problem, "authentication-unavailable") &&
cockpit_authorize_type (session->authorization, &type) &&
g_str_equal (type, "negotiate"))
{
g_debug ("%s: negotiate authentication not available", session->name);
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Negotiate authentication not available");
gssapi_available = 0;
}
else if (g_str_equal (problem, "authentication-failed") ||
g_str_equal (problem, "authentication-unavailable"))
{
cockpit_json_get_object (options, "auth-method-results", NULL, &auth_results);
if (auth_results)
{
cockpit_json_get_string (auth_results, "password", NULL, &pw_result);
if (!pw_result || g_strcmp0 (pw_result, "no-server-support") == 0)
{
g_clear_error (error);
g_set_error (error, COCKPIT_ERROR,
COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication failed: authentication-not-supported");
}
}
if (*error == NULL)
{
g_debug ("%s: %s %s", session->name, problem, message);
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication failed");
}
}
else if (g_str_equal (problem, "no-host") ||
g_str_equal (problem, "invalid-hostkey") ||
g_str_equal (problem, "unknown-hostkey") ||
g_str_equal (problem, "unknown-host") ||
g_str_equal (problem, "terminated"))
{
g_debug ("%s: %s", session->name, problem);
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication failed: %s", problem);
}
else if (g_str_equal (problem, "access-denied"))
{
g_debug ("permission denied %s", message);
g_set_error_literal (error, COCKPIT_ERROR, COCKPIT_ERROR_PERMISSION_DENIED,
message ? message : "Permission denied");
}
else
{
g_debug ("%s: errored %s: %s", session->name, problem, message);
if (message)
{
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Authentication failed: %s: %s", problem, message);
}
else
{
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Authentication failed: %s", problem);
}
}
free (type);
}
static gboolean
on_transport_control (CockpitTransport *transport,
const char *command,
const gchar *channel,
JsonObject *options,
GBytes *payload,
gpointer user_data)
{
CockpitSession *session = user_data;
const gchar *problem = NULL;
const gchar *message = NULL;
GSimpleAsyncResult *result;
GError *error = NULL;
gboolean ret = TRUE;
if (g_str_equal (command, "init"))
{
g_debug ("session initialized");
g_signal_handler_disconnect (session->transport, session->control_sig);
g_signal_handler_disconnect (session->transport, session->close_sig);
session->control_sig = session->close_sig = 0;
session->initialized = TRUE;
if (cockpit_json_get_string (options, "problem", NULL, &problem) && problem)
{
if (!cockpit_json_get_string (options, "message", NULL, &message))
message = NULL;
propagate_problem_to_error (session, options, problem, message, &error);
if (session->result)
{
g_simple_async_result_take_error (session->result, error);
}
else
{
g_message ("ignoring failure from session process: %s", error->message);
g_error_free (error);
}
}
ret = FALSE; /* Let this message be handled elsewhere */
}
else if (g_str_equal (command, "authorize"))
{
g_debug ("received authorize challenge");
if (session->authorize)
json_object_unref (session->authorize);
session->authorize = json_object_ref (options);
if (reply_authorize_challenge (session))
return TRUE;
}
else
{
g_message ("unexpected \"%s\" control message from session before \"init\"", command);
cockpit_transport_close (transport, "protocol-error");
}
if (session->result)
{
result = session->result;
session->result = NULL;
g_simple_async_result_complete (result);
g_object_unref (result);
}
return ret;
}
static void
on_transport_closed (CockpitTransport *transport,
const gchar *problem,
gpointer user_data)
{
CockpitSession *session = user_data;
GSimpleAsyncResult *result;
CockpitPipe *pipe;
GError *error = NULL;
gint status = 0;
if (g_strcmp0 (problem, "timeout") == 0)
{
g_message ("%s: authentication timed out", session->name);
g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Authentication failed: Timeout");
}
else if (!session->initialized)
{
pipe = cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (transport));
if (cockpit_pipe_get_pid (pipe, NULL))
status = cockpit_pipe_exit_status (pipe);
g_debug ("%s: authentication process exited: %d", session->name, status);
if (problem)
{
g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Internal error in login process");
}
else
{
g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication failed");
}
}
if (!error)
{
g_message ("%s: authentication process failed", session->name);
g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Authentication internal error");
}
if (session->result)
{
result = session->result;
session->result = NULL;
g_simple_async_result_take_error (result, error);
g_simple_async_result_complete (result);
g_object_unref (result);
}
else
{
g_message ("ignoring failure from session process: %s", error->message);
cockpit_session_reset (session);
g_error_free (error);
}
}
static CockpitCreds *
build_session_credentials (CockpitAuth *self,
GIOStream *connection,
GHashTable *headers,
const char *application,
const char *type,
const char *authorization)
{
CockpitCreds *creds;
const gchar *authorized;
char *user = NULL;
char *raw = NULL;
GBytes *password = NULL;
gchar *remote_peer = NULL;
gchar *csrf_token = NULL;
/* Prepare various credentials */
if (g_strcmp0 (type, "basic") == 0)
{
raw = cockpit_authorize_parse_basic (authorization, &user);
/* If "password" is in the X-Authorize header, we can keep the password for use */
authorized = g_hash_table_lookup (headers, "X-Authorize");
if (!authorized)
authorized = "";
if (strstr (authorized, "password") && raw)
{
password = g_bytes_new_take (raw, strlen (raw));
raw = NULL;
}
}
remote_peer = get_remote_address (connection);
csrf_token = cockpit_auth_nonce (self);
creds = cockpit_creds_new (application,
COCKPIT_CRED_USER, user,
COCKPIT_CRED_PASSWORD, password,
COCKPIT_CRED_RHOST, remote_peer,
COCKPIT_CRED_CSRF_TOKEN, csrf_token,
NULL);
g_free (remote_peer);
if (raw)
{
cockpit_memory_clear (raw, strlen (raw));
free (raw);
}
g_free (csrf_token);
if (password)
g_bytes_unref (password);
free (user);
return creds;
}
static gboolean
on_session_timeout (gpointer data)
{
CockpitSession *session = data;
session->timeout_tag = 0;
if (!session->service || cockpit_web_service_get_idling (session->service))
{
g_info ("session timed out");
cockpit_session_reset (session);
}
return FALSE;
}
static void
on_web_service_idling (CockpitWebService *service,
gpointer data)
{
CockpitSession *session = data;
if (session->timeout_tag)
g_source_remove (session->timeout_tag);
g_debug ("session is idle");
/*
* The minimum amount of time before a request uses this new web service,
* otherwise it will just go away.
*/
session->timeout_tag = g_timeout_add_seconds (cockpit_ws_service_idle,
on_session_timeout,
session);
/*
* Also reset the timer which checks whether anything is going on in the
* entire process or not.
*/
if (session->auth->timeout_tag)
g_source_remove (session->auth->timeout_tag);
session->auth->timeout_tag = g_timeout_add_seconds (cockpit_ws_process_idle,
on_process_timeout, session->auth);
}
static void
on_web_service_destroy (CockpitWebService *service,
gpointer data)
{
on_web_service_idling (service, data);
cockpit_session_reset (data);
}
static CockpitSession *
cockpit_session_create (CockpitAuth *self,
GIOStream *connection,
GHashTable *headers,
const gchar *type,
const gchar *authorization,
const gchar *application,
GError **error)
{
CockpitTransport *transport = NULL;
CockpitSession *session = NULL;
CockpitCreds *creds = NULL;
const gchar *host;
const gchar *action;
const gchar *command;
const gchar *section;
const gchar *program_default;
gchar **env = g_get_environ ();
const gchar *argv[] = {
"command",
"host",
NULL,
};
host = application_parse_host (application);
action = type_option (type, "action", "localhost");
if (g_strcmp0 (action, ACTION_NONE) == 0)
{
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication disabled");
goto out;
}
/* These are the credentials we'll carry around for this session */
creds = build_session_credentials (self, connection, headers,
application, type, authorization);
if (host)
section = COCKPIT_CONF_SSH_SECTION;
else if (self->login_loopback && g_strcmp0 (type, "basic") == 0)
section = COCKPIT_CONF_SSH_SECTION;
else if (g_strcmp0 (action, ACTION_SSH) == 0)
section = COCKPIT_CONF_SSH_SECTION;
else
section = type;
if (g_strcmp0 (section, COCKPIT_CONF_SSH_SECTION) == 0)
{
if (!host)
host = type_option (COCKPIT_CONF_SSH_SECTION, "host", "127.0.0.1");
program_default = cockpit_ws_ssh_program;
}
else
{
program_default = cockpit_ws_session_program;
}
command = type_option (section, "command", program_default);
if (cockpit_creds_get_rhost (creds))
{
env = g_environ_setenv (env, "COCKPIT_REMOTE_PEER",
cockpit_creds_get_rhost (creds),
TRUE);
}
argv[0] = command;
argv[1] = host ? host : "localhost";
transport = session_start_process (argv, (const gchar **)env);
if (!transport)
{
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Authentication failed to start");
goto out;
}
session = g_new0 (CockpitSession, 1);
session->refs = 1;
session->name = g_path_get_basename (argv[0]);
session->auth = self;
session->service = cockpit_web_service_new (creds, transport);
session->idling_sig = g_signal_connect (session->service, "idling",
G_CALLBACK (on_web_service_idling), session);
session->destroy_sig = g_signal_connect (session->service, "destroy",
G_CALLBACK (on_web_service_destroy), session);
g_object_weak_ref (G_OBJECT (session->service), on_web_service_gone, session);
session->transport = g_object_ref (transport);
session->control_sig = g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), session);
session->close_sig = g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), session);
/* How long to wait for the auth process to send some data */
session->authorize_timeout = timeout_option ("timeout", section, cockpit_ws_auth_process_timeout);
/* How long to wait for a response from the client to a auth prompt */
session->client_timeout = timeout_option ("response-timeout", section, cockpit_ws_auth_response_timeout);
out:
g_strfreev (env);
if (creds)
cockpit_creds_unref (creds);
if (transport)
g_object_unref (transport);
return session;
}
static gboolean
build_authorize_challenge (CockpitAuth *self,
JsonObject *authorize,
GIOStream *connection,
GHashTable *headers,
JsonObject **body,
gchar **conversation)
{
const gchar *challenge = NULL;
gchar *type = NULL;
JsonObject *object;
GList *l, *names;
if (!cockpit_json_get_string (authorize, "challenge", NULL, &challenge) ||
!cockpit_authorize_type (challenge, &type))
{
g_message ("invalid \"challenge\" field in \"authorize\" message");
return FALSE;
}
g_hash_table_replace (headers, g_strdup ("WWW-Authenticate"), g_strdup (challenge));
*conversation = NULL;
if (g_str_equal (type, "negotiate"))
{
gssapi_available = 1;
*conversation = cockpit_auth_nonce (self);
if (connection)
g_object_set_data_full (G_OBJECT (connection), "negotiate", g_strdup (*conversation), g_free);
}
else if (g_str_equal (type, "x-conversation"))
{
cockpit_authorize_subject (challenge, conversation);
}
object = json_object_new ();
names = json_object_get_members (authorize);
for (l = names; l != NULL; l = g_list_next (l))
{
if (!g_str_equal (l->data, "challenge") && !g_str_equal (l->data, "cookie"))
json_object_set_member (object, l->data, json_object_dup_member (authorize, l->data));
}
if (body)
*body = object;
g_list_free (names);
g_free (type);
return TRUE;
}
static void
authorize_logger (const char *data)
{
g_message ("%s", data);
}
static void
cockpit_auth_class_init (CockpitAuthClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = cockpit_auth_finalize;
sig__idling = g_signal_new ("idling", COCKPIT_TYPE_AUTH, G_SIGNAL_RUN_FIRST,
0, NULL, NULL, NULL, G_TYPE_NONE, 0);
cockpit_authorize_logger (authorize_logger, 0);
}
static char *
base64_decode_string (const char *enc)
{
if (enc == NULL)
return NULL;
char *dec = g_strdup (enc);
gsize len;
g_base64_decode_inplace (dec, &len);
dec[len] = '\0';
return dec;
}
static CockpitSession *
session_for_headers (CockpitAuth *self,
const gchar *path,
GHashTable *in_headers)
{
gchar *cookie = NULL;
gchar *raw = NULL;
const char *prefix = "v=2;k=";
CockpitSession *ret = NULL;
gchar *application;
gchar *cookie_name = NULL;
g_return_val_if_fail (self != NULL, FALSE);
g_return_val_if_fail (in_headers != NULL, FALSE);
application = cockpit_auth_parse_application (path, NULL);
if (!application)
return NULL;
cookie_name = application_cookie_name (application);
raw = cockpit_web_server_parse_cookie (in_headers, cookie_name);
if (raw)
{
cookie = base64_decode_string (raw);
if (cookie != NULL)
{
if (g_str_has_prefix (cookie, prefix))
ret = g_hash_table_lookup (self->sessions, cookie);
else
g_debug ("invalid or unsupported cookie: %s", cookie);
g_free (cookie);
}
g_free (raw);
}
g_free (application);
g_free (cookie_name);
return ret;
}
CockpitWebService *
cockpit_auth_check_cookie (CockpitAuth *self,
const gchar *path,
GHashTable *in_headers)
{
CockpitSession *session;
CockpitCreds *creds;
session = session_for_headers (self, path, in_headers);
if (session)
{
creds = cockpit_web_service_get_creds (session->service);
g_debug ("received %s credential cookie for session",
cockpit_creds_get_application (creds));
return g_object_ref (session->service);
}
else
{
g_debug ("received unknown/invalid credential cookie");
return NULL;
}
}
/*
* returns TRUE if auth can proceed, FALSE otherwise.
* dropping starts at connection max_startups_begin with a probability
* of (max_startups_rate/100). the probability increases linearly until
* all connections are dropped for startups > max_startups
*/
static gboolean
can_start_auth (CockpitAuth *self)
{
int p, r;
/* 0 means unlimited */
if (self->max_startups == 0)
return TRUE;
/* Under soft limit */
if (self->startups <= self->max_startups_begin)
return TRUE;
/* Over hard limit */
if (self->startups > self->max_startups)
return FALSE;
/* If rate is 100, soft limit is hard limit */
if (self->max_startups_rate == 100)
return FALSE;
p = 100 - self->max_startups_rate;
p *= self->startups - self->max_startups_begin;
p /= self->max_startups - self->max_startups_begin;
p += self->max_startups_rate;
r = g_random_int_range (0, 100);
g_debug ("calculating if auth can start: (%u:%u:%u): p %d, r %d",
self->max_startups_begin, self->max_startups_rate,
self->max_startups, p, r);
return (r < p) ? FALSE : TRUE;
}
void
cockpit_auth_login_async (CockpitAuth *self,
const gchar *path,
GIOStream *connection,
GHashTable *headers,
GAsyncReadyCallback callback,
gpointer user_data)
{
GSimpleAsyncResult *result = NULL;
CockpitSession *session;
GError *error = NULL;
gchar *type = NULL;
gchar *conversation = NULL;
gchar *authorization = NULL;
gchar *application = NULL;
g_return_if_fail (path != NULL);
g_return_if_fail (headers != NULL);
self->startups++;
result = g_simple_async_result_new (G_OBJECT (self), callback, user_data,
cockpit_auth_login_async);
if (!can_start_auth (self))
{
g_message ("Request dropped; too many startup connections: %u", self->startups);
g_simple_async_result_set_error (result, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
"Connection closed by host");
g_simple_async_result_complete_in_idle (result);
goto out;
}
application = cockpit_auth_parse_application (path, NULL);
authorization = cockpit_auth_steal_authorization (headers, connection, &type, &conversation);
if (!application || !authorization)
{
g_simple_async_result_set_error (result, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication required");
g_simple_async_result_complete_in_idle (result);
goto out;
}
if (conversation)
{
session = g_hash_table_lookup (self->conversations, conversation);
if (!session)
{
g_simple_async_result_set_error (result, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Invalid conversation token");
g_simple_async_result_complete_in_idle (result);
goto out;
}
g_simple_async_result_set_op_res_gpointer (result, cockpit_session_ref (session), cockpit_session_unref);
}
else
{
session = cockpit_session_create (self, connection, headers, type, authorization, application, &error);
if (!session)
{
g_simple_async_result_take_error (result, error);
g_simple_async_result_complete_in_idle (result);
goto out;
}
g_simple_async_result_set_op_res_gpointer (result, session, cockpit_session_unref);
}
cockpit_session_reset (session);
session->result = g_object_ref (result);
session->authorization = authorization;
authorization = NULL;
if (conversation && !reply_authorize_challenge (session))
{
g_simple_async_result_set_error (result, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Invalid conversation reply");
g_simple_async_result_complete_in_idle (result);
goto out;
}
reset_authorize_timeout (session, FALSE);
out:
g_free (type);
g_free (application);
g_free (conversation);
if (authorization)
{
cockpit_memory_clear (authorization, -1);
g_free (authorization);
}
g_object_unref (result);
}
JsonObject *
cockpit_auth_login_finish (CockpitAuth *self,
GAsyncResult *result,
GIOStream *connection,
GHashTable *headers,
GError **error)
{
JsonObject *body = NULL;
CockpitCreds *creds = NULL;
CockpitSession *session = NULL;
gboolean force_secure;
gchar *cookie_name;
gchar *cookie_b64;
gchar *header;
gchar *id;
g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self),
cockpit_auth_login_async), NULL);
g_object_ref (result);
session = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error))
goto out;
g_return_val_if_fail (session != NULL, NULL);
g_return_val_if_fail (session->result == NULL, NULL);
cockpit_session_reset (session);
if (session->authorize)
{
if (build_authorize_challenge (self, session->authorize, connection,
headers, &body, &session->conversation))
{
if (session->conversation)
{
reset_authorize_timeout (session, TRUE);
g_hash_table_replace (self->conversations, session->conversation, cockpit_session_ref (session));
}
}
}
if (session->initialized)
{
/* Start off in the idling state, and begin a timeout during which caller must do something else */
on_web_service_idling (session->service, session);
creds = cockpit_web_service_get_creds (session->service);
id = cockpit_auth_nonce (self);
session->cookie = g_strdup_printf ("v=2;k=%s", id);
g_hash_table_insert (self->sessions, session->cookie, cockpit_session_ref (session));
g_free (id);
if (headers)
{
force_secure = connection ? !G_IS_SOCKET_CONNECTION (connection) : TRUE;
cookie_name = application_cookie_name (cockpit_creds_get_application (creds));
cookie_b64 = g_base64_encode ((guint8 *)session->cookie, strlen (session->cookie));
header = g_strdup_printf ("%s=%s; Path=/; %s HttpOnly",
cookie_name, cookie_b64,
force_secure ? " Secure;" : "");
g_free (cookie_b64);
g_free (cookie_name);
g_hash_table_insert (headers, g_strdup ("Set-Cookie"), header);
}
if (body)
json_object_unref (body);
body = cockpit_creds_to_json (creds);
}
else
{
g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication failed");
}
out:
self->startups--;
g_object_unref (result);
/* Successful login */
if (creds)
g_info ("logged in user session");
return body;
}
CockpitAuth *
cockpit_auth_new (gboolean login_loopback)
{
CockpitAuth *self = g_object_new (COCKPIT_TYPE_AUTH, NULL);
const gchar *max_startups_conf;
gint count = 0;
self->login_loopback = login_loopback;
if (cockpit_ws_max_startups == NULL)
max_startups_conf = cockpit_conf_string ("WebService", "MaxStartups");
else
max_startups_conf = cockpit_ws_max_startups;
self->max_startups = max_startups;
self->max_startups_begin = max_startups;
self->max_startups_rate = 100;
if (max_startups_conf)
{
count = sscanf (max_startups_conf, "%u:%u:%u",
&self->max_startups_begin,
&self->max_startups_rate,
&self->max_startups);
/* If all three numbers are not given use the
* first as a hard limit */
if (count == 1 || count == 2)
{
self->max_startups = self->max_startups_begin;
self->max_startups_rate = 100;
}
if (count < 1 || count > 3 ||
self->max_startups_begin > self->max_startups ||
self->max_startups_rate > 100 || self->max_startups_rate < 1)
{
g_warning ("Illegal MaxStartups spec: %s. Reverting to defaults", max_startups_conf);
self->max_startups = max_startups;
self->max_startups_begin = max_startups;
self->max_startups_rate = 100;
}
}
return self;
}
gchar *
cockpit_auth_parse_application (const gchar *path,
gboolean *is_host)
{
const gchar *pos;
gchar *tmp = NULL;
gchar *val = NULL;
g_return_val_if_fail (path != NULL, NULL);
g_return_val_if_fail (path[0] == '/', NULL);
path += 1;
/* We are being embedded as a specific application */
if (g_str_has_prefix (path, "cockpit+") && path[8] != '\0')
{
pos = strchr (path, '/');
if (pos)
val = g_strndup (path, pos - path);
else
val = g_strdup (path);
}
else if (path[0] == '=' && path[1] != '\0')
{
pos = strchr (path, '/');
if (pos)
{
tmp = g_strndup (path, pos - path);
val = g_strdup_printf ("cockpit+%s", tmp);
}
else
{
val = g_strdup_printf ("cockpit+%s", path);
}
}
else
{
val = g_strdup ("cockpit");
}
if (is_host)
*is_host = application_parse_host (val) != NULL;
g_free (tmp);
return val;
}