/* * 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 "cockpitchannel.h" #include "cockpitdbusinternal.h" #include "cockpitdbusjson.h" #include "cockpitechochannel.h" #include "cockpitfslist.h" #include "cockpitfsread.h" #include "cockpitfswatch.h" #include "cockpitfsreplace.h" #include "cockpithttpstream.h" #include "cockpitinteracttransport.h" #include "cockpitnullchannel.h" #include "cockpitpackages.h" #include "cockpitpipechannel.h" #include "cockpitinternalmetrics.h" #include "cockpitpolkitagent.h" #include "cockpitrouter.h" #include "cockpitwebsocketstream.h" #include "common/cockpitassets.h" #include "common/cockpitjson.h" #include "common/cockpitlog.h" #include "common/cockpitpipetransport.h" #include "common/cockpitsystem.h" #include "common/cockpittest.h" #include "common/cockpitunixfd.h" #include "common/cockpitwebresponse.h" #include #include #include #include #include #include #include #include #include /* This program is run on each managed server, with the credentials of the user that is logged into the Server Console. */ static CockpitPackages *packages = NULL; static CockpitPayloadType payload_types[] = { { "dbus-json3", cockpit_dbus_json_get_type }, { "http-stream1", cockpit_http_stream_get_type }, { "http-stream2", cockpit_http_stream_get_type }, { "stream", cockpit_pipe_channel_get_type }, { "fsread1", cockpit_fsread_get_type }, { "fsreplace1", cockpit_fsreplace_get_type }, { "fswatch1", cockpit_fswatch_get_type }, { "fslist1", cockpit_fslist_get_type }, { "null", cockpit_null_channel_get_type }, { "echo", cockpit_echo_channel_get_type }, { "websocket-stream1", cockpit_web_socket_stream_get_type }, { NULL }, }; static void add_router_channels (CockpitRouter *router) { JsonObject *match; match = json_object_new (); json_object_set_string_member (match, "payload", "metrics1"); json_object_set_string_member (match, "source", "internal"); cockpit_router_add_channel (router, match, cockpit_internal_metrics_get_type); json_object_unref (match); } static void on_closed_set_flag (CockpitTransport *transport, const gchar *problem, gpointer user_data) { gboolean *flag = user_data; *flag = TRUE; } static gboolean on_logout_set_flag (CockpitTransport *transport, const gchar *command, const gchar *channel, JsonObject *options, GBytes *payload, gpointer user_data) { gboolean *flag = user_data; if (g_str_equal (command, "logout")) *flag = TRUE; return FALSE; } static void send_init_command (CockpitTransport *transport, gboolean interactive) { const gchar *checksum; JsonObject *object; JsonObject *block; GHashTable *os_release; gchar **names; GBytes *bytes; gint i; object = json_object_new (); json_object_set_string_member (object, "command", "init"); json_object_set_int_member (object, "version", 1); /* * When in interactive mode pretend we received an init * message, and don't print one out. */ if (interactive) { json_object_set_string_member (object, "host", "localhost"); } else { checksum = cockpit_packages_get_checksum (packages); if (checksum) json_object_set_string_member (object, "checksum", checksum); /* This is encoded as an object to allow for future expansion */ block = json_object_new (); names = cockpit_packages_get_names (packages); for (i = 0; names && names[i] != NULL; i++) json_object_set_null_member (block, names[i]); json_object_set_object_member (object, "packages", block); g_free (names); os_release = cockpit_system_load_os_release (); block = cockpit_json_from_hash_table (os_release, cockpit_system_os_release_fields ()); if (block) json_object_set_object_member (object, "os-release", block); g_hash_table_unref (os_release); } bytes = cockpit_json_write_bytes (object); json_object_unref (object); if (interactive) cockpit_transport_emit_recv (transport, NULL, bytes); else cockpit_transport_send (transport, NULL, bytes); g_bytes_unref (bytes); } static void setup_dbus_daemon (gpointer addrfd) { g_unsetenv ("G_DEBUG"); cockpit_unix_fd_close_all (3, GPOINTER_TO_INT (addrfd)); } static GPid start_dbus_daemon (void) { GError *error = NULL; GString *address = NULL; gchar *line; gsize len; gssize ret; GPid pid = 0; gchar *print_address = NULL; int addrfd[2] = { -1, -1 }; GSpawnFlags flags; gchar *dbus_argv[] = { "dbus-daemon", "--print-address=X", "--session", NULL }; if (pipe (addrfd)) { g_warning ("pipe failed to allocate fds: %m"); goto out; } print_address = g_strdup_printf ("--print-address=%d", addrfd[1]); dbus_argv[1] = print_address; /* The DBus daemon produces useless messages on stderr mixed in */ flags = G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL | G_SPAWN_STDOUT_TO_DEV_NULL; g_spawn_async_with_pipes (NULL, dbus_argv, NULL, flags, setup_dbus_daemon, GINT_TO_POINTER (addrfd[1]), &pid, NULL, NULL, NULL, &error); close (addrfd[1]); if (error != NULL) { if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) g_debug ("couldn't start %s: %s", dbus_argv[0], error->message); else g_message ("couldn't start %s: %s", dbus_argv[0], error->message); g_error_free (error); pid = 0; goto out; } g_debug ("launched %s", dbus_argv[0]); address = g_string_new (""); for (;;) { len = address->len; g_string_set_size (address, len + 256); ret = read (addrfd[0], address->str + len, 256); if (ret < 0) { g_string_set_size (address, len); if (errno != EAGAIN && errno != EINTR) { g_warning ("couldn't read address from dbus-daemon: %s", g_strerror (errno)); goto out; } } else if (ret == 0) { g_string_set_size (address, len); break; } else { g_string_set_size (address, len + ret); line = strchr (address->str, '\n'); if (line != NULL) { *line = '\0'; break; } } } if (address->str[0] == '\0') { g_message ("dbus-daemon didn't send us a dbus address; not installed?"); } else { g_setenv ("DBUS_SESSION_BUS_ADDRESS", address->str, TRUE); g_debug ("session bus address: %s", address->str); } out: if (addrfd[0] >= 0) close (addrfd[0]); if (address) g_string_free (address, TRUE); g_free (print_address); return pid; } static void setup_ssh_agent (gpointer addrfd) { g_unsetenv ("G_DEBUG"); prctl (PR_SET_PDEATHSIG, SIGTERM); cockpit_unix_fd_close_all (3, GPOINTER_TO_INT (addrfd)); } static GPid start_ssh_agent (void) { GError *error = NULL; GPid pid = 0; gint fd = -1; gint status = -1; gchar *pid_line = NULL; gchar *agent_output = NULL; gchar *agent_error = NULL; gchar *bind_address = g_strdup_printf ("%s/ssh-agent.XXXXXX", g_get_user_runtime_dir ()); gchar *agent_argv[] = { "ssh-agent", "-a", bind_address, NULL }; fd = g_mkstemp (bind_address); if (fd < 0) { g_warning ("couldn't create temporary socket file: %s", g_strerror (errno)); goto out; } if (g_unlink (bind_address) < 0) { g_warning ("couldn't remove temporary socket file: %s", g_strerror (errno)); goto out; } if (!g_spawn_sync (NULL, agent_argv, NULL, G_SPAWN_SEARCH_PATH, setup_ssh_agent, GINT_TO_POINTER (-1), &agent_output, &agent_error, &status, &error)) { if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) g_debug ("couldn't start %s: %s", agent_argv[0], error->message); else g_warning ("couldn't start %s: %s", agent_argv[0], error->message); goto out; } if (!g_spawn_check_exit_status (status, &error)) { g_warning ("couldn't start %s: %s: %s", agent_argv[0], error->message, agent_error); goto out; } pid_line = strstr (agent_output, "SSH_AGENT_PID="); if (pid_line) { if (sscanf (pid_line, "SSH_AGENT_PID=%d;", &pid) != 1) { g_warning ("couldn't find pid in %s", pid_line); goto out; } } if (pid < 1) { g_warning ("couldn't get agent pid from ssh-agent output: %s", agent_output); goto out; } g_debug ("launched %s", agent_argv[0]); g_setenv ("SSH_AUTH_SOCK", bind_address, TRUE); out: g_clear_error (&error); if (fd >= 0) close (fd); g_free (bind_address); g_free (agent_error); g_free (agent_output); return pid; } static gboolean have_env (const gchar *name) { const gchar *env = g_getenv (name); return env && env[0]; } static gboolean on_signal_done (gpointer data) { gboolean *closed = data; *closed = TRUE; return TRUE; } static struct passwd * getpwuid_a (uid_t uid) { int err; long bufsize = sysconf (_SC_GETPW_R_SIZE_MAX); struct passwd *ret = NULL; struct passwd *buf; if (bufsize <= 0) bufsize = 8192; buf = g_malloc (sizeof(struct passwd) + bufsize); err = getpwuid_r (uid, buf, (char *)(buf + 1), bufsize, &ret); if (ret == NULL) { free (buf); if (err == 0) err = ENOENT; errno = err; } return ret; } static void update_router (CockpitRouter *router, gboolean privileged_slave) { if (!privileged_slave) { GList *bridges = cockpit_packages_get_bridges (packages); cockpit_router_set_bridges (router, bridges); g_list_free (bridges); } } static CockpitRouter * setup_router (CockpitTransport *transport, gboolean privileged_slave) { CockpitRouter *router = NULL; packages = cockpit_packages_new (); router = cockpit_router_new (transport, payload_types, NULL); add_router_channels (router); /* This has to happen after add_router_channels as the * packages based bridges should have priority. */ update_router (router, privileged_slave); return router; } struct CallUpdateRouterData { CockpitRouter *router; gboolean privileged_slave; }; static void call_update_router (gconstpointer user_data) { const struct CallUpdateRouterData *data = user_data; update_router (data->router, data->privileged_slave); } static int run_bridge (const gchar *interactive, gboolean privileged_slave) { CockpitTransport *transport; CockpitRouter *router; gboolean terminated = FALSE; gboolean interupted = FALSE; gboolean closed = FALSE; gpointer polkit_agent = NULL; const gchar *directory; struct passwd *pwd; GPid daemon_pid = 0; GPid agent_pid = 0; guint sig_term; guint sig_int; int outfd; uid_t uid; struct CallUpdateRouterData call_update_router_data; cockpit_set_journal_logging (G_LOG_DOMAIN, !isatty (2)); /* Always set environment variables early */ uid = geteuid(); pwd = getpwuid_a (uid); if (pwd == NULL) { g_message ("couldn't get user info: %s", g_strerror (errno)); } else { g_setenv ("USER", pwd->pw_name, TRUE); g_setenv ("HOME", pwd->pw_dir, TRUE); g_setenv ("SHELL", pwd->pw_shell, TRUE); } /* Set a path if nothing is set */ g_setenv ("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 0); /* * The bridge always runs from within $XDG_RUNTIME_DIR * This makes it easy to create user sockets and/or files. */ if (!privileged_slave) { directory = g_get_user_runtime_dir (); if (g_mkdir_with_parents (directory, 0700) < 0) g_warning ("couldn't create runtime dir: %s: %s", directory, g_strerror (errno)); else if (g_chdir (directory) < 0) g_warning ("couldn't change to runtime dir: %s: %s", directory, g_strerror (errno)); } /* Reset the umask, typically this is done in .bashrc for a login shell */ umask (022); /* * This process talks on stdin/stdout. However lots of stuff wants to write * to stdout, such as g_debug, and uses fd 1 to do that. Reroute fd 1 so that * it goes to stderr, and use another fd for stdout. */ outfd = dup (1); if (outfd < 0 || dup2 (2, 1) < 1) { g_warning ("bridge couldn't redirect stdout to stderr"); if (outfd > -1) close (outfd); outfd = 1; } sig_term = g_unix_signal_add (SIGTERM, on_signal_done, &terminated); sig_int = g_unix_signal_add (SIGINT, on_signal_done, &interupted); g_type_init (); /* Start daemons if necessary */ if (!interactive && !privileged_slave) { if (!have_env ("DBUS_SESSION_BUS_ADDRESS")) daemon_pid = start_dbus_daemon (); if (!have_env ("SSH_AUTH_SOCK")) agent_pid = start_ssh_agent (); } cockpit_dbus_internal_startup (interactive != NULL); if (interactive) { /* Allow skipping the init message when interactive */ transport = cockpit_interact_transport_new (0, outfd, interactive); } else { transport = cockpit_pipe_transport_new_fds ("stdio", 0, outfd); } if (uid != 0) { if (!interactive) polkit_agent = cockpit_polkit_agent_register (transport, NULL); } g_resources_register (cockpitassets_get_resource ()); cockpit_web_failure_resource = "/org/cockpit-project/Cockpit/fail.html"; if (privileged_slave) { /* * When in privileged mode, exit right away when we get any sort of logout * This enforces the fact that the user no longer has privileges. */ g_signal_connect (transport, "control", G_CALLBACK (on_logout_set_flag), &closed); } router = setup_router (transport, privileged_slave); cockpit_dbus_user_startup (pwd); cockpit_dbus_setup_startup (); cockpit_dbus_process_startup (); cockpit_dbus_machines_startup (); cockpit_packages_dbus_startup (packages); call_update_router_data.router = router; call_update_router_data.privileged_slave = privileged_slave; cockpit_packages_on_change (packages, call_update_router, &call_update_router_data); g_free (pwd); pwd = NULL; g_signal_connect (transport, "closed", G_CALLBACK (on_closed_set_flag), &closed); send_init_command (transport, interactive ? TRUE : FALSE); while (!terminated && !closed && !interupted) g_main_context_iteration (NULL, TRUE); if (polkit_agent) cockpit_polkit_agent_unregister (polkit_agent); g_object_unref (router); g_object_unref (transport); cockpit_packages_on_change (packages, NULL, NULL); cockpit_dbus_machines_cleanup (); cockpit_dbus_internal_cleanup (); if (daemon_pid) kill (daemon_pid, SIGTERM); if (agent_pid) kill (agent_pid, SIGTERM); g_source_remove (sig_term); g_source_remove (sig_int); /* So the caller gets the right signal */ if (terminated) raise (SIGTERM); return 0; } static void print_rules (gboolean opt_privileged) { CockpitRouter *router = NULL; CockpitTransport *transport = cockpit_interact_transport_new (0, 1, "--"); router = setup_router (transport, opt_privileged); cockpit_router_dump_rules (router); g_object_unref (router); g_object_unref (transport); } static void print_version (void) { gint i, offset, len; g_print ("Version: %s\n", PACKAGE_VERSION); g_print ("Protocol: 1\n"); g_print ("Payloads: "); offset = 10; for (i = 0; payload_types[i].name != NULL; i++) { len = strlen (payload_types[i].name); if (offset + len > 70) { g_print ("\n"); offset = 0; } if (offset == 0) { g_print (" "); offset = 4; }; g_print ("%s ", payload_types[i].name); offset += len + 1; } g_print ("\n"); g_print ("Authorization: crypt1\n"); } int main (int argc, char **argv) { GOptionContext *context; GError *error = NULL; int ret; static gboolean opt_packages = FALSE; static gboolean opt_rules = FALSE; static gboolean opt_privileged = FALSE; static gboolean opt_version = FALSE; static gchar *opt_interactive = NULL; static GOptionEntry entries[] = { { "interact", 0, 0, G_OPTION_ARG_STRING, &opt_interactive, "Interact with the raw protocol", "boundary" }, { "privileged", 0, 0, G_OPTION_ARG_NONE, &opt_privileged, "Privileged copy of bridge", NULL }, { "packages", 0, 0, G_OPTION_ARG_NONE, &opt_packages, "Show Cockpit package information", NULL }, { "rules", 0, 0, G_OPTION_ARG_NONE, &opt_rules, "Show Cockpit bridge rules", NULL }, { "version", 0, 0, G_OPTION_ARG_NONE, &opt_version, "Show Cockpit version information", NULL }, { NULL } }; signal (SIGPIPE, SIG_IGN); /* Debugging issues during testing */ signal (SIGABRT, cockpit_test_signal_backtrace); signal (SIGSEGV, cockpit_test_signal_backtrace); /* * We have to tell GLib about an alternate default location for XDG_DATA_DIRS * if we've been compiled with a different prefix. GLib caches that, so need * to do this very early. */ if (!g_getenv ("XDG_DATA_DIRS") && !g_str_equal (DATADIR, "/usr/share")) g_setenv ("XDG_DATA_DIRS", DATADIR, TRUE); g_setenv ("LANG", "en_US.UTF-8", FALSE); g_setenv ("GSETTINGS_BACKEND", "memory", TRUE); g_setenv ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE); g_setenv ("GIO_USE_VFS", "local", TRUE); context = g_option_context_new (NULL); g_option_context_add_main_entries (context, entries, NULL); g_option_context_set_description (context, "cockpit-bridge is run automatically inside of a Cockpit session. When\n" "run from the command line one of the options above must be specified.\n"); g_option_context_parse (context, &argc, &argv, &error); g_option_context_free (context); if (error) { g_printerr ("cockpit-bridge: %s\n", error->message); g_error_free (error); return 1; } if (opt_packages) { cockpit_packages_dump (); return 0; } else if (opt_rules) { print_rules (opt_privileged); return 0; } else if (opt_version) { print_version (); return 0; } if (!opt_interactive && isatty (1)) { g_printerr ("cockpit-bridge: no option specified\n"); return 2; } ret = run_bridge (opt_interactive, opt_privileged); if (packages) cockpit_packages_free (packages); g_free (opt_interactive); return ret; }