tls: Use socket activation instead of spawning

Instead of fork()/exec()ing the cockpit-ws instances for ourselves,
simply connect to the sockets that systemd is now creating for us.

In addition to shrinking the code by about 600 lines, this has the
following advantages:

  - the cockpit-ws instances are now running as a separate user for
    improved isolation

  - the cockpit-ws instances are now also each in their own cgroup

  - there used to be a race where we would drop an incoming web
    connection if we attempted to connect to a cockpit-ws instance as it
    was exiting due to inactivity.  systemd socket activation avoids
    this.

Use the socket-activation-helper to tweak the tests to adjust to the
changes.

This temporarily drops the per-client-certificate ws instances, there is
only one https instance for now; this is not a practical regression as
we don't support client certificates yet. This will be brought back
separately.
This commit is contained in:
Allison Karlitskaya 2019-09-27 14:10:56 +02:00 committed by Martin Pitt
parent 777c59095a
commit 708671171d
26 changed files with 207 additions and 922 deletions

4
.gitignore vendored
View File

@ -107,8 +107,8 @@ depcomp
/mock/
*.min.html
/src/ws/cockpit.appdata.xml
/src/ws/cockpit.service
/src/ws/cockpit.socket
/src/ws/cockpit*.service
/src/ws/cockpit*.socket
/src/ws/cockpit-tempfiles.conf
src/common/cockpitassets.c
src/common/cockpitassets.h

View File

@ -119,6 +119,8 @@ SUBST_RULE = \
-e 's,[@]SUDO[@],$(SUDO),g' \
-e 's,[@]user[@],$(COCKPIT_USER),g' \
-e 's,[@]group[@],$(COCKPIT_GROUP),g' \
-e 's,[@]wsinstanceuser[@],$(COCKPIT_WSINSTANCE_USER),g' \
-e 's,[@]wsinstancegroup[@],$(COCKPIT_WSINSTANCE_GROUP),g' \
-e 's,[@]selinux_config_type[@],$(COCKPIT_SELINUX_CONFIG_TYPE),g' \
-e 's,[@]with_networkmanager_needs_root[@],$(with_networkmanager_needs_root),g' \
-e 's,[@]with_storaged_iscsi_sessions[@],$(with_storaged_iscsi_sessions),g' \

View File

@ -410,7 +410,7 @@ fi
AM_CONDITIONAL(WITH_ASAN, test "$enable_asan" = "yes")
AC_MSG_RESULT($asan_status)
# User and group for running cockpit-ws
# User and group for running cockpit web server (cockpit-tls or -ws in customized setups)
AC_ARG_WITH(cockpit_user,
AS_HELP_STRING([--with-cockpit-user=<user>],
@ -437,6 +437,35 @@ fi
AC_SUBST(COCKPIT_USER)
AC_SUBST(COCKPIT_GROUP)
# User for running cockpit-ws instances from cockpit-tls
AC_ARG_WITH(cockpit_ws_instance_user,
AS_HELP_STRING([--with-cockpit-ws-instance-user=<user>],
[User for running cockpit-ws instances from cockpit-tls (root)]
)
)
AC_ARG_WITH(cockpit_ws_instance_group,
AS_HELP_STRING([--with-cockpit-ws-instance-group=<group>],
[Group for running cockpit-ws instances from cockpit-tls]
)
)
if test -z "$with_cockpit_ws_instance_user"; then
if test "$COCKPIT_USER" != "root"; then
AC_MSG_ERROR([--with-cockpit-ws-instance-user is required when setting --with-cockpit-user])
fi
COCKPIT_WSINSTANCE_USER=root
else
COCKPIT_WSINSTANCE_USER=$with_cockpit_ws_instance_user
if test -z "$with_cockpit_ws_instance_group"; then
COCKPIT_WSINSTANCE_GROUP=$with_cockpit_ws_instance_user
else
COCKPIT_WSINSTANCE_GROUP=$with_cockpit_ws_instance_group
fi
fi
AC_SUBST(COCKPIT_WSINSTANCE_USER)
AC_SUBST(COCKPIT_WSINSTANCE_GROUP)
AC_ARG_WITH(selinux_config_type,
AS_HELP_STRING([--with-selinux-config-type=<type>],
[SELinux context type for cockpit config files]
@ -586,6 +615,8 @@ echo "
cockpit-ws user: ${COCKPIT_USER}
cockpit-ws group: ${COCKPIT_GROUP}
cockpit-ws instance user: ${COCKPIT_WSINSTANCE_USER}
cockpit-ws instance group: ${COCKPIT_WSINSTANCE_GROUP}
selinux config type: ${COCKPIT_SELINUX_CONFIG_TYPE}
Maintainer mode: ${USE_MAINTAINER_MODE}
@ -595,7 +626,7 @@ echo "
With coverage: ${enable_coverage}
With address sanitizer: ${asan_status}
With PCP: ${enable_pcp}
Branding: ${BRAND}
Branding: ${BRAND}
cockpit-ssh: ${enable_ssh}
Supports key auth: ${key_auth}

View File

@ -2,8 +2,6 @@ libexec_PROGRAMS += cockpit-tls
cockpit_tls_SOURCES = \
src/tls/utils.h \
src/tls/wsinstance.h \
src/tls/wsinstance.c \
src/tls/connection.h \
src/tls/connection.c \
src/tls/server.h \
@ -34,7 +32,6 @@ TEST_LDADD = \
TLS_TESTS = \
test-tls-connection \
test-tls-wsinstance \
test-tls-server \
$(NULL)
@ -50,16 +47,7 @@ test_tls_connection_SOURCES = \
test_tls_connection_CFLAGS = $(TEST_CFLAGS)
test_tls_connection_LDADD = $(TEST_LDADD)
test_tls_wsinstance_SOURCES = \
src/tls/wsinstance.c \
src/tls/test-wsinstance.c \
$(NULL)
test_tls_wsinstance_CFLAGS = $(TEST_CFLAGS)
test_tls_wsinstance_LDADD = $(TEST_LDADD)
test_tls_server_SOURCES = \
src/tls/wsinstance.c \
src/tls/connection.c \
src/tls/server.c \
src/tls/test-server.c \

View File

@ -40,6 +40,7 @@ connection_new (int client_fd)
con->client_fd = client_fd;
con->buf_client.connection = con;
con->buf_ws.connection = con;
con->ws_fd = -1;
debug (CONNECTION, "new connection on fd %i", con->client_fd);
return con;

View File

@ -20,8 +20,7 @@
#pragma once
#include <stdbool.h>
#include "wsinstance.h"
#include <gnutls/gnutls.h>
typedef enum { CLIENT, WS } DataSource;
typedef enum { SUCCESS, PARTIAL, CLOSED, RETRY, FATAL } ConnectionResult;
@ -41,7 +40,6 @@ struct Connection {
int client_fd;
bool is_tls;
gnutls_session_t session;
WsInstance *ws;
int ws_fd;
struct ConnectionBuffer buf_client;
struct ConnectionBuffer buf_ws;

View File

@ -30,8 +30,6 @@
#include "utils.h"
#include "server.h"
#define COCKPIT_WS PACKAGE_LIBEXEC_DIR "/cockpit-ws"
/* CLI arguments */
struct arguments {
uint16_t port;
@ -112,7 +110,7 @@ main (int argc, char **argv)
}
/* TODO: Add cockpit.conf option to enable client-certificate auth, once we support that */
server_init (COCKPIT_WS, arguments.port, certfile, NULL, CERT_NONE);
server_init ("/run/cockpit/wsinstance", arguments.port, certfile, NULL, CERT_NONE);
free (certfile);
server_run (arguments.idle_timeout);

View File

@ -36,7 +36,6 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/wait.h>
#include <gnutls/gnutls.h>
@ -45,7 +44,6 @@
#include <common/cockpitmemory.h>
#include <common/cockpitwebcertificate.h>
#include "utils.h"
#include "wsinstance.h"
#include "connection.h"
#include "server.h"
@ -56,16 +54,12 @@
static struct {
bool initialized;
const char *ws_path;
const char *state_dir;
enum ClientCertMode client_cert_mode;
int listen_fds[MAX_LISTEN_FDS];
gnutls_certificate_credentials_t x509_cred;
gnutls_priority_t priority_cache;
Connection *connections;
WsInstance *wss; /* cockpit-ws instances, one for each client certificate */
WsInstance *ws_notls; /* cockpit-ws instance for unencrypted HTTP */
int epollfd;
struct sigaction old_sigchld;
} server;
/***********************************
@ -87,40 +81,6 @@ static struct {
rval = cmd; \
} while(rval == GNUTLS_E_AGAIN || rval == GNUTLS_E_INTERRUPTED)
static int cleanup_children_eventfd = -1;
static void
handle_sigchld (int signal)
{
const uint64_t one = 1;
ssize_t s;
s = write (cleanup_children_eventfd, &one, sizeof one);
assert (s == sizeof one);
}
static void
handle_child_exit (void)
{
uint64_t value;
ssize_t s;
debug (SERVER, "got SIGCHLD");
s = read (cleanup_children_eventfd, &value, sizeof value);
assert (s == sizeof value);
for (;;)
{
int status;
pid_t pid = waitpid (-1, &status, WNOHANG);
if (pid <= 0)
break;
debug (SERVER, "pid %u exited with status %x", pid, status);
server_remove_ws (pid);
}
}
/**
* check_sd_listen_pid: Verify that systemd-activated socket is for us
*
@ -161,7 +121,7 @@ check_sd_listen_pid (void)
* @ws: If given, all connections related to this #WsInstance get removed
*/
static void
remove_connection (int fd, WsInstance *ws)
remove_connection (int fd)
{
Connection *c, *cprev;
bool found = false;
@ -170,12 +130,12 @@ remove_connection (int fd, WsInstance *ws)
{
Connection *cnext = c->next;
if ( (fd > 0 && (c->client_fd == fd || c->ws_fd == fd)) || (ws && c->ws == ws) )
if (fd > 0 && (c->client_fd == fd || c->ws_fd == fd))
{
/* stop polling it */
if (epoll_ctl (server.epollfd, EPOLL_CTL_DEL, c->client_fd, NULL) < 0)
err (1, "Failed to remove epoll connection fd");
if (c->ws_fd)
if (c->ws_fd != -1)
{
if (epoll_ctl (server.epollfd, EPOLL_CTL_DEL, c->ws_fd, NULL) < 0)
err (1, "Failed to remove epoll connection ws fd");
@ -197,7 +157,7 @@ remove_connection (int fd, WsInstance *ws)
}
if (!found)
debug (CONNECTION, "remove_connection: fd %i or ws %s not found in connections", fd, ws ? ws->socket.sun_path : "(unset)");
debug (CONNECTION, "remove_connection: fd %i not found in connections", fd);
}
/**
@ -211,70 +171,38 @@ static void
connection_init_ws (Connection *c)
{
int fd;
const gnutls_datum_t *peer_der = NULL;
WsInstance *ws = NULL;
bool ws_add_tls = false;
bool ws_add_notls = false;
struct epoll_event ev = { .events = EPOLLIN };
struct sockaddr_un sockaddr = { .sun_family = AF_UNIX };
const char *sockname;
int r;
if (c->is_tls)
{
peer_der = gnutls_certificate_get_peers (c->session, NULL);
/* find existing ws server for this peer cert */
for (ws = server.wss; ws; ws = ws->next)
if (ws_instance_has_peer_cert (ws, peer_der))
break;
if (!ws)
{
ws = ws_instance_new (server.ws_path, WS_INSTANCE_HTTPS, peer_der, server.state_dir);
ws_add_tls = true;
}
}
sockname = "https";
else
{
ws = server.ws_notls;
if (!ws)
{
debug (CONNECTION, "initializing no-TLS cockpit-ws instance");
ws = ws_instance_new (server.ws_path,
server.x509_cred ? WS_INSTANCE_HTTP_REDIRECT : WS_INSTANCE_HTTP,
NULL, server.state_dir);
ws_add_notls = true;
}
}
sockname = server.x509_cred ? "http-redirect" : "http";
debug (CONNECTION, "connection_init_ws: assigned ws %s", ws->socket.sun_path);
r = snprintf (sockaddr.sun_path, sizeof sockaddr.sun_path, "%s/%s.sock", server.ws_path, sockname);
assert (r < sizeof sockaddr.sun_path);
debug (CONNECTION, "connection_init_ws: assigned ws %s", sockaddr.sun_path);
/* connect to ws instance */
fd = socket (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0)
err (1, "failed to create cockpit-ws client socket");
if (connect (fd, (struct sockaddr *) &ws->socket, sizeof (ws->socket)) < 0)
if (connect (fd, (struct sockaddr *) &sockaddr, sizeof sockaddr) < 0)
{
/* cockpit-ws crashed? */
warn ("failed to connect to cockpit-ws");
ws_instance_free (ws, true);
return;
}
/* connected, so it's valid; add it to our ws list */
if (ws_add_tls)
{
ws->next = server.wss;
server.wss = ws;
}
if (ws_add_notls)
server.ws_notls = ws;
/* epoll the fd */
ev.data.ptr = &c->buf_ws;
if (epoll_ctl (server.epollfd, EPOLL_CTL_ADD, fd, &ev) < 0)
err (1, "Failed to epoll cockpit-ws client fd");
c->ws_fd = fd;
c->ws = ws;
}
/**
@ -365,7 +293,7 @@ handle_connection_data_first (Connection *con)
char b;
int ret;
assert (!con->ws);
assert (con->ws_fd == -1);
/* peek the first byte and see if it's a TLS connection (starting with 22).
We can assume that there is some data to read, as this is called in response
@ -376,7 +304,7 @@ handle_connection_data_first (Connection *con)
if (ret == 0) /* EOF */
{
debug (CONNECTION, "client disconnected without sending any data");
remove_connection (con->client_fd, NULL);
remove_connection (con->client_fd);
return;
}
@ -389,7 +317,7 @@ handle_connection_data_first (Connection *con)
if (!server.x509_cred)
{
warnx ("got TLS connection, but our server does not have a certificate/key; refusing");
remove_connection (con->client_fd, NULL);
remove_connection (con->client_fd);
return;
}
@ -411,7 +339,7 @@ handle_connection_data_first (Connection *con)
if (ret < 0)
{
warnx ("TLS handshake failed: %s", gnutls_strerror (ret));
remove_connection (con->client_fd, NULL);
remove_connection (con->client_fd);
return;
}
@ -419,8 +347,8 @@ handle_connection_data_first (Connection *con)
}
connection_init_ws (con);
if (!con->ws)
remove_connection (con->client_fd, NULL);
if (con->ws_fd == -1)
remove_connection (con->client_fd);
}
/**
@ -439,13 +367,12 @@ handle_connection_data (struct ConnectionBuffer *buf)
ConnectionResult r;
assert (con);
debug (CONNECTION, "%s connection fd %i has data from %s; ws %s",
debug (CONNECTION, "%s connection fd %i has data from %s",
con->is_tls ? "TLS" : "unencrypted", con->client_fd,
src == WS ? "ws" : "client",
con->ws ? con->ws->socket.sun_path : "uninitialized");
src == WS ? "ws" : "client");
/* first data on a new connection; determine if TLS, init TLS, and assign a ws */
if (!con->ws)
if (con->ws_fd == -1)
{
assert (src == CLIENT);
handle_connection_data_first (con);
@ -465,7 +392,7 @@ handle_connection_data (struct ConnectionBuffer *buf)
}
if (r != SUCCESS)
remove_connection (con->client_fd, NULL);
remove_connection (con->client_fd);
}
static void
@ -474,7 +401,7 @@ handle_hangup (struct ConnectionBuffer *buf)
Connection *con = buf->connection;
int fd = buf == &con->buf_client ? con->client_fd : con->ws_fd;
debug (CONNECTION, "hangup on fd %i", fd);
remove_connection (fd, NULL);
remove_connection (fd);
}
/***********************************
@ -489,7 +416,7 @@ handle_hangup (struct ConnectionBuffer *buf)
* There is only one instance of this. Trying to initialize it more than once
* is an error.
*
* @ws_path: Path to cockpit-ws binary
* @ws_path: Path to cockpit-wsinstance sockets directory
* @port: Port to listen to; ignored when the listening socket is handed over
* through the systemd socket activation protocol
* @certfile: Server TLS certificate file; if %NULL, TLS is not supported.
@ -507,21 +434,12 @@ server_init (const char *ws_path,
int ret;
const char *env_listen_fds;
struct epoll_event ev = { .events = EPOLLIN };
const struct sigaction child_action = {
.sa_handler = handle_sigchld,
.sa_flags = SA_NOCLDSTOP
};
assert (!server.initialized);
server.ws_path = ws_path;
server.client_cert_mode = client_cert_mode;
/* Initialize state dir for ws instances; $RUNTIME_DIRECTORY is set by systemd's RuntimeDirectory=, or by tests */
server.state_dir = secure_getenv ("RUNTIME_DIRECTORY");
if (!server.state_dir)
err (1, "$RUNTIME_DIRECTORY environment variable must be set to a private directory");
/* Initialize TLS */
if (certfile)
{
@ -607,18 +525,6 @@ server_init (const char *ws_path,
err (1, "Failed to epoll server listening fd");
}
/* track cockpit-ws children */
cleanup_children_eventfd = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);
if (cleanup_children_eventfd == -1)
err (1, "failed to create eventfd");
ev.data.ptr = handle_child_exit;
if (epoll_ctl (server.epollfd, EPOLL_CTL_ADD, cleanup_children_eventfd, &ev) < 0)
err (1, "Failed to epoll add sigchld handler fd");
if (sigaction (SIGCHLD, &child_action, &server.old_sigchld) < 0)
err (1, "Failed to set up SIGCHLD handler");
server.initialized = true;
}
@ -637,12 +543,6 @@ server_cleanup (void)
assert (server.initialized);
if (sigaction (SIGCHLD, &server.old_sigchld, NULL) < 0)
err (1, "Failed to reset SIGCHLD handler");
close (cleanup_children_eventfd);
cleanup_children_eventfd = -1;
for (Connection *c = server.connections; c; )
{
Connection *cnext = c->next;
@ -650,15 +550,6 @@ server_cleanup (void)
c = cnext;
}
for (WsInstance *ws = server.wss; ws; )
{
WsInstance *wsnext = ws->next;
ws_instance_free (ws, true);
ws = wsnext;
}
if (server.ws_notls)
ws_instance_free (server.ws_notls, true);
if (server.x509_cred)
{
gnutls_certificate_free_credentials (server.x509_cred);
@ -698,8 +589,6 @@ server_poll_event (int timeout)
{
if (ev.data.ptr >= (void *) server.listen_fds && ev.data.ptr < (void *) &server.listen_fds[MAX_LISTEN_FDS])
handle_accept (* ((int *) ev.data.ptr));
else if (ev.data.ptr == handle_child_exit)
handle_child_exit ();
else if (ev.events & EPOLLIN)
handle_connection_data (ev.data.ptr);
else if (ev.events & EPOLLHUP)
@ -739,54 +628,6 @@ server_run (int idle_timeout)
}
}
/**
* server_remove_ws: Clean up #WsInstance
*
* This should be called in response to a SIGCHLD signal, i. e. when a
* cockpit-ws terminates. This also terminates and cleans up all Connections
* from this cockpit-ws instance.
*/
void
server_remove_ws (pid_t ws_pid)
{
WsInstance *ws = NULL;
assert (server.initialized);
/* find the WsInstance of that pid */
if (server.ws_notls && server.ws_notls->pid == ws_pid)
{
ws = server.ws_notls;
server.ws_notls = NULL;
}
else
{
WsInstance *wsprev = NULL;
for (ws = server.wss; ws; wsprev = ws, ws = ws->next)
{
if (ws->pid == ws_pid)
{
if (wsprev == NULL) /* first ws */
server.wss = ws->next;
else
wsprev->next = ws->next;
break;
}
}
}
if (!ws)
{
warnx ("server_remove_ws: pid %u not found in our ws instances", ws_pid);
return;
}
debug (SERVER, "server_remove_ws: pid %u is ws %s", ws_pid, ws->socket.sun_path);
remove_connection (-1, ws);
ws_instance_free (ws, false);
}
unsigned
server_num_connections (void)
{
@ -796,34 +637,3 @@ server_num_connections (void)
count++;
return count;
}
unsigned
server_num_ws (void)
{
unsigned count = 0;
for (WsInstance *ws = server.wss; ws; ws = ws->next)
count++;
if (server.ws_notls)
count++;
return count;
}
size_t
server_get_ws_pids (pid_t* pids, size_t pids_length)
{
size_t num = 0;
if (server.ws_notls)
{
assert (pids_length > num);
pids[num++] = server.ws_notls->pid;
}
for (WsInstance *ws = server.wss; ws; ws = ws->next)
{
assert (pids_length > num);
pids[num++] = ws->pid;
}
return num;
}

View File

@ -49,8 +49,7 @@ test_no_ws (void)
/* other fields are clear */
g_assert (!c->is_tls);
g_assert (c->ws == NULL);
g_assert_cmpint (c->ws_fd, ==, 0);
g_assert_cmpint (c->ws_fd, ==, -1);
connection_free (c);
/* closes fd */

View File

@ -20,6 +20,7 @@
#include "config.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
@ -34,6 +35,7 @@
#include "server.h"
#include "common/cockpittest.h"
#define SOCKET_ACTIVATION_HELPER BUILDDIR "/socket-activation-helper"
#define COCKPIT_WS BUILDDIR "/cockpit-ws"
#define CERTFILE SRCDIR "/src/bridge/mock-server.crt"
#define KEYFILE SRCDIR "/src/bridge/mock-server.key"
@ -47,7 +49,8 @@
const unsigned server_port = 9123;
typedef struct {
gchar *state_dir;
gchar *ws_socket_dir;
GPid ws_spawner;
struct sockaddr_in server_addr;
} TestCase;
@ -159,7 +162,7 @@ assert_https_outcome (TestCase *tc,
bool expect_tls_failure)
{
pid_t pid;
int status;
int status = -1;
block_sigchld ();
@ -242,7 +245,7 @@ assert_https_outcome (TestCase *tc,
exit (0);
}
while (waitpid (pid, &status, WNOHANG) <= 0)
for (int retry = 0; retry < 100 && waitpid (pid, &status, WNOHANG) <= 0; ++retry)
server_poll_event (50);
g_assert_cmpint (status, ==, 0);
}
@ -253,40 +256,30 @@ assert_https (TestCase *tc, const char *client_crt, const char *client_key, unsi
assert_https_outcome (tc, client_crt, client_key, expected_server_certs, false);
}
/* Ensure that all ws instances have no blocked signals inherited from cockpit-tls */
static void
assert_children_signals (void)
{
/* only use that for tests with a small number of ws instances */
const size_t max_ws = 5;
pid_t ws_pids[max_ws];
size_t num_ws;
/* this does not work under valgrind */
if (strstr (g_getenv ("LD_PRELOAD") ?: "", "valgrind") != NULL)
return;
num_ws = server_get_ws_pids (ws_pids, max_ws);
for (size_t i = 0; i < num_ws; ++i)
{
g_autofree gchar *contents = NULL;
g_autofree gchar *path = g_strdup_printf ("/proc/%u/status", ws_pids[i]);
g_assert (g_file_get_contents (path, &contents, NULL, NULL));
if (!g_regex_match_simple ("^SigBlk:\\s*0+$", contents, G_REGEX_MULTILINE, 0))
g_error ("Non-zero SigBlk in process %u: %s", ws_pids[i], contents);
}
}
static void
setup (TestCase *tc, gconstpointer data)
{
const TestFixture *fixture = data;
g_autoptr(GError) error = NULL;
tc->state_dir = g_dir_make_tmp ("server.state.XXXXXX", NULL);
g_assert (tc->state_dir);
g_assert (g_setenv ("RUNTIME_DIRECTORY", tc->state_dir, TRUE));
tc->ws_socket_dir = g_dir_make_tmp ("server.wssock.XXXXXX", NULL);
g_assert (tc->ws_socket_dir);
server_init (COCKPIT_WS,
gchar* sah_argv[] = { SOCKET_ACTIVATION_HELPER, COCKPIT_WS, tc->ws_socket_dir, NULL };
if (!g_spawn_async (NULL, sah_argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &tc->ws_spawner, &error))
g_error ("Failed to spawn " SOCKET_ACTIVATION_HELPER ": %s", error->message);
/* wait until socket activation helper is ready */
int socket_dir_fd = open (tc->ws_socket_dir, O_RDONLY | O_DIRECTORY);
for (int retry = 0; retry < 200; ++retry)
{
if (faccessat (socket_dir_fd, "ready", F_OK, 0) == 0)
break;
g_usleep (10000);
}
close (socket_dir_fd);
server_init (tc->ws_socket_dir,
server_port,
fixture ? fixture->certfile : NULL,
fixture ? fixture->keyfile : NULL,
@ -300,26 +293,24 @@ static void
teardown (TestCase *tc, gconstpointer data)
{
server_cleanup ();
/* all server children got cleaned up */
g_assert_cmpint (kill (tc->ws_spawner, SIGTERM), ==, 0);
g_assert_cmpint (waitpid (tc->ws_spawner, NULL, 0), ==, tc->ws_spawner);
/* all children got cleaned up */
g_assert_cmpint (wait (NULL), ==, -1);
g_assert_cmpint (errno, ==, ECHILD);
g_assert_cmpuint (server_num_ws (), ==, 0);
/* connection should fail */
g_assert_cmpint (do_connect (tc), ==, -ECONNREFUSED);
g_unsetenv ("COCKPIT_WS_PROCESS_IDLE");
g_assert_cmpint (g_rmdir (tc->state_dir), ==, 0);
g_free (tc->state_dir);
}
static void
test_no_tls_immediate_shutdown (TestCase *tc, gconstpointer data)
{
g_assert_cmpuint (server_num_ws (), ==, 0);
g_assert_cmpuint (server_num_connections (), ==, 0);
assert_http (tc);
g_assert_cmpuint (server_num_ws (), ==, 1);
g_assert_cmpuint (server_num_connections (), ==, 1);
assert_children_signals ();
int socket_dir_fd = open (tc->ws_socket_dir, O_RDONLY | O_DIRECTORY);
g_assert_cmpint (unlinkat (socket_dir_fd, "http.sock", 0), ==, 0);
g_assert_cmpint (unlinkat (socket_dir_fd, "http-redirect.sock", 0), ==, 0);
g_assert_cmpint (unlinkat (socket_dir_fd, "https.sock", 0), ==, 0);
g_assert_cmpint (unlinkat (socket_dir_fd, "ready", 0), ==, 0);
close (socket_dir_fd);
g_assert_cmpint (g_rmdir (tc->ws_socket_dir), ==, 0);
g_free (tc->ws_socket_dir);
}
static void
@ -331,17 +322,13 @@ test_no_tls_con_shutdown (TestCase *tc, gconstpointer data)
for (int retries = 0; retries < 10 && server_num_connections () == 1; ++retries)
server_run (100);
g_assert_cmpuint (server_num_connections (), ==, 0);
g_assert_cmpuint (server_num_ws (), ==, 1);
}
static void
test_no_tls_many_serial (TestCase *tc, gconstpointer data)
{
g_assert_cmpuint (server_num_ws (), ==, 0);
for (int i = 0; i < 20; ++i)
assert_http (tc);
/* should all be served by the same ws */
g_assert_cmpuint (server_num_ws (), ==, 1);
}
static void
@ -351,7 +338,6 @@ test_no_tls_many_parallel (TestCase *tc, gconstpointer data)
block_sigchld ();
g_assert_cmpuint (server_num_ws (), ==, 0);
for (i = 0; i < 20; ++i)
{
pid_t pid = fork ();
@ -396,9 +382,6 @@ test_no_tls_many_parallel (TestCase *tc, gconstpointer data)
--i;
}
}
/* all served by the same ws */
g_assert_cmpuint (server_num_ws (), ==, 1);
}
static void
@ -417,7 +400,6 @@ static void
test_tls_no_client_cert (TestCase *tc, gconstpointer data)
{
assert_https (tc, NULL, NULL, 1);
assert_children_signals ();
}
static void
@ -434,32 +416,23 @@ test_tls_redirect (TestCase *tc, gconstpointer data)
/* with TLS support it should redirect */
const char *res = do_request (tc, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n");
cockpit_assert_strmatch (res, "HTTP/1.1 301 Moved Permanently*");
assert_children_signals ();
}
static void
test_tls_client_cert (TestCase *tc, gconstpointer data)
{
g_assert_cmpuint (server_num_ws (), ==, 0);
assert_https (tc, CLIENT_CERTFILE, CLIENT_KEYFILE, 1);
g_assert_cmpuint (server_num_ws (), ==, 1);
/* no-cert case is handled by separate ws */
assert_https (tc, NULL, NULL, 1);
g_assert_cmpuint (server_num_ws (), ==, 2);
assert_https (tc, CLIENT_CERTFILE, CLIENT_KEYFILE, 1);
g_assert_cmpuint (server_num_ws (), ==, 2);
assert_children_signals ();
}
static void
test_tls_client_cert_disabled (TestCase *tc, gconstpointer data)
{
g_assert_cmpuint (server_num_ws (), ==, 0);
assert_https (tc, CLIENT_CERTFILE, CLIENT_KEYFILE, 1);
g_assert_cmpuint (server_num_ws (), ==, 1);
/* no-cert case is handled by same ws, as client certs are disabled server-side */
assert_https (tc, NULL, NULL, 1);
g_assert_cmpuint (server_num_ws (), ==, 1);
}
static void
@ -483,41 +456,10 @@ test_tls_cert_chain (TestCase *tc, gconstpointer data)
static void
test_mixed_protocols (TestCase *tc, gconstpointer data)
{
g_assert_cmpuint (server_num_ws (), ==, 0);
assert_https (tc, NULL, NULL, 1);
g_assert_cmpuint (server_num_ws (), ==, 1);
assert_http (tc);
g_assert_cmpuint (server_num_ws (), ==, 2);
assert_https (tc, NULL, NULL, 1);
g_assert_cmpuint (server_num_ws (), ==, 2);
assert_http (tc);
g_assert_cmpuint (server_num_ws (), ==, 2);
}
static void
test_ws_idle (TestCase *tc, gconstpointer data)
{
g_assert (g_setenv ("COCKPIT_WS_PROCESS_IDLE", "2", TRUE));
assert_http (tc);
g_assert_cmpuint (server_num_ws (), ==, 1);
g_assert_cmpint (waitpid (0, NULL, WNOHANG), ==, 0);
/* ws process should disappear after idle wait */
sleep (3);
/* run the mainloop to collect the zombie */
while (server_poll_event (0))
;
/* process is gone */
g_assert_cmpint (waitpid (0, NULL, WNOHANG), ==, -1);
g_assert_cmpint (errno, ==, ECHILD);
g_assert_cmpuint (server_num_ws (), ==, 0);
/* a new request should re-spawn ws */
assert_http (tc);
g_assert_cmpuint (server_num_ws (), ==, 1);
}
static void
@ -536,8 +478,6 @@ main (int argc, char *argv[])
{
cockpit_test_init (&argc, &argv);
g_test_add ("/server/no-tls/immediate-shutdown", TestCase, NULL,
setup, test_no_tls_immediate_shutdown, teardown);
g_test_add ("/server/no-tls/process-connection-shutdown", TestCase, NULL,
setup, test_no_tls_con_shutdown, teardown);
g_test_add ("/server/no-tls/many-serial", TestCase, NULL,
@ -564,8 +504,6 @@ main (int argc, char *argv[])
setup, test_tls_redirect, teardown);
g_test_add ("/server/mixed-protocols", TestCase, &fixture_separate_crt_key,
setup, test_mixed_protocols, teardown);
g_test_add ("/server/ws-idle", TestCase, NULL,
setup, test_ws_idle, teardown);
g_test_add ("/server/run-idle", TestCase, NULL,
setup, test_run_idle, teardown);

View File

@ -1,278 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <gnutls/x509.h>
#include "wsinstance.h"
#include "common/cockpittest.h"
#define COCKPIT_WS BUILDDIR "/cockpit-ws"
#define WS_SUCCESS "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n*"
#define WS_FORBIDDEN "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n*"
typedef struct {
gchar *state_dir;
WsInstance *ws;
} TestCase;
typedef struct {
enum WsInstanceMode mode;
const char *cert_pem;
} TestFixture;
static void
setup (TestCase *tc, gconstpointer data)
{
const TestFixture *fixture = data;
gnutls_datum_t peer_der;
tc->state_dir = g_dir_make_tmp ("wsinstance.state.XXXXXX", NULL);
g_assert (tc->state_dir);
if (fixture->cert_pem)
{
gnutls_datum_t peer_pem;
gnutls_x509_crt_t peer_cert;
gsize cert_pem_length;
g_assert (g_file_get_contents (fixture->cert_pem, (gchar**) &peer_pem.data, &cert_pem_length, NULL));
peer_pem.size = cert_pem_length;
g_assert (gnutls_x509_crt_init (&peer_cert) >= 0);
g_assert (gnutls_x509_crt_import (peer_cert, &peer_pem, GNUTLS_X509_FMT_PEM) >= 0);
g_assert (gnutls_x509_crt_export2 (peer_cert, GNUTLS_X509_FMT_DER, &peer_der) >= 0);
gnutls_x509_crt_deinit (peer_cert);
gnutls_free (peer_pem.data);
}
tc->ws = ws_instance_new (COCKPIT_WS, fixture->mode, fixture->cert_pem ? &peer_der : NULL, tc->state_dir);
if (fixture->cert_pem)
gnutls_free (peer_der.data);
/* process is running */
g_assert_cmpint (kill (tc->ws->pid, 0), ==, 0);
/* socket exists */
g_assert (g_file_test (tc->ws->socket.sun_path, G_FILE_TEST_EXISTS));
g_assert (g_str_has_prefix (tc->ws->socket.sun_path, tc->state_dir));
}
static void
teardown (TestCase *tc, gconstpointer data)
{
pid_t ws_pid = tc->ws->pid;
g_autofree char *socket_path = g_strdup (tc->ws->socket.sun_path);
ws_instance_free (tc->ws, true);
/* process is not running any more */
g_assert_cmpint (kill (ws_pid, 0), ==, -1);
g_assert_cmpint (errno, ==, ESRCH);
/* socket got cleaned up */
g_assert (!g_file_test (socket_path, G_FILE_TEST_EXISTS));
g_assert_cmpint (g_rmdir (tc->state_dir), ==, 0);
g_free (tc->state_dir);
}
static int
connect_to_ws (TestCase *tc)
{
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
g_assert (fd > 0);
g_assert (connect (fd, (const struct sockaddr*) &tc->ws->socket, sizeof (tc->ws->socket)) == 0);
return fd;
}
static const char*
do_request (TestCase *tc, const char *request)
{
int fd = connect_to_ws (tc);
static char buf[4096];
ssize_t len;
g_assert_cmpint (write (fd, request, strlen (request)), ==, strlen (request));
len = read (fd, buf, sizeof (buf) - 1);
close (fd);
g_assert_cmpint (len, >=, 100);
buf[len] = '\0';
return buf;
}
static void
assert_http (TestCase *tc)
{
const char *res = do_request (tc, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n");
/* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */
if (strstr (res, "200 OK"))
cockpit_assert_strmatch (res, "HTTP/1.1 200 OK\r\n*");
else
cockpit_assert_strmatch (res, "HTTP/1.1 404 Not Found\r\n*");
}
static void
assert_websocket (TestCase *tc, const char *origin, const char *expected)
{
const char request[] = "GET /socket HTTP/1.0\r\n"
"Host: localhost:9090\r\n"
"Connection: Upgrade\r\n"
"Upgrade: websocket\r\n"
"Sec-Websocket-Key: 3sc2c9IzwRUc3BlSIYwtSA==\r\n"
"Sec-Websocket-Version: 13\r\n"
"Origin: ";
char buf[4096] = "\0";
int fd = connect_to_ws (tc);
g_assert_cmpint (write (fd, request, sizeof (request) - 1), ==, sizeof (request) - 1);
g_assert_cmpint (write (fd, origin, strlen (origin)), ==, strlen (origin));
g_assert_cmpint (write (fd, "\r\n\r\n", 4), ==, 4);
g_assert_cmpint (read (fd, buf, sizeof (buf)), >=, 50);
close (fd);
cockpit_assert_strmatch (buf, expected);
}
static const TestFixture fixture_http = {
.mode = WS_INSTANCE_HTTP,
};
static void
test_http (TestCase *tc, gconstpointer data)
{
const char *res;
gnutls_datum_t crt = { .size = 0 };
g_assert_cmpuint (tc->ws->peer_cert.size, ==, 0);
g_assert_cmpuint (tc->ws->peer_cert_info.size, ==, 0);
assert_http (tc);
assert_websocket (tc, "http://localhost:9090", WS_SUCCESS);
assert_websocket (tc, "https://localhost:9090", WS_FORBIDDEN);
g_assert (ws_instance_has_peer_cert (tc->ws, NULL));
g_assert (ws_instance_has_peer_cert (tc->ws, &crt));
crt.size = 1;
g_assert (!ws_instance_has_peer_cert (tc->ws, &crt));
/* non-localhost does not redirect */
res = do_request (tc, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n");
/* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */
if (strstr (res, "200 OK"))
cockpit_assert_strmatch (res, "HTTP/1.1 200 OK\r\n*");
else
cockpit_assert_strmatch (res, "HTTP/1.1 404 Not Found\r\n*");
}
static const TestFixture fixture_http_redirect = {
.mode = WS_INSTANCE_HTTP_REDIRECT,
};
static void
test_http_redirect (TestCase *tc, gconstpointer data)
{
/* localhost does not redirect */
assert_http (tc);
/* non-localhost redirects */
cockpit_assert_strmatch (do_request (tc, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n"),
"HTTP/1.1 301 Moved Permanently*");
}
static const TestFixture fixture_https_nocert = {
.mode = WS_INSTANCE_HTTPS,
};
static void
test_https_nocert (TestCase *tc, gconstpointer data)
{
g_assert_cmpuint (tc->ws->peer_cert.size, ==, 0);
g_assert_cmpuint (tc->ws->peer_cert_info.size, ==, 0);
assert_http (tc);
assert_websocket (tc, "https://localhost:9090", WS_SUCCESS);
assert_websocket (tc, "http://localhost:9090", WS_FORBIDDEN);
}
static const TestFixture fixture_https_cert = {
.mode = WS_INSTANCE_HTTPS,
.cert_pem = SRCDIR "/src/bridge/mock-server.crt",
};
static void
test_https_cert (TestCase *tc, gconstpointer data)
{
const TestFixture *fixture = data;
g_autofree char *cert_file_path = NULL;
g_autofree char *cert_file = NULL;
g_autofree char *expected_pem = NULL;
gnutls_datum_t crt = { .size = 0 };
g_assert_cmpuint (tc->ws->peer_cert.size, >, 0);
g_assert (tc->ws->peer_cert.data != NULL);
cockpit_assert_strmatch ((const char*) tc->ws->peer_cert_info.data, "subject `CN=localhost', issuer `CN=localhost', *");
assert_http (tc);
assert_websocket (tc, "https://localhost:9090", WS_SUCCESS);
assert_websocket (tc, "http://localhost:9090", WS_FORBIDDEN);
g_assert (!ws_instance_has_peer_cert (tc->ws, NULL));
g_assert (!ws_instance_has_peer_cert (tc->ws, &crt));
g_assert (ws_instance_has_peer_cert (tc->ws, &tc->ws->peer_cert));
/* certificate copy should match */
crt.size = tc->ws->peer_cert.size;
crt.data = malloc (tc->ws->peer_cert.size);
g_assert (crt.data);
memcpy (crt.data, tc->ws->peer_cert.data, crt.size);
g_assert (ws_instance_has_peer_cert (tc->ws, &crt));
/* modified crt should not match */
crt.data[0]++;
g_assert (!ws_instance_has_peer_cert (tc->ws, &crt));
/* writes expected certificate file */
cert_file_path = g_strdup_printf ("%s/ws.%u.crt", tc->state_dir, tc->ws->pid);
g_assert (g_file_get_contents (cert_file_path, &cert_file, NULL, NULL));
g_assert (g_file_get_contents (fixture->cert_pem, &expected_pem, NULL, NULL));
g_assert_cmpstr (g_strchomp (cert_file), ==, g_strchomp (expected_pem));
gnutls_free (crt.data);
}
int
main (int argc, char *argv[])
{
cockpit_test_init (&argc, &argv);
g_test_add ("/ws-instance/http", TestCase, &fixture_http,
setup, test_http, teardown);
g_test_add ("/ws-instance/http-redirect", TestCase, &fixture_http_redirect,
setup, test_http_redirect, teardown);
g_test_add ("/ws-instance/tls-nocert", TestCase, &fixture_https_nocert,
setup, test_https_nocert, teardown);
g_test_add ("/ws-instance/tls-cert", TestCase, &fixture_https_cert,
setup, test_https_cert, teardown);
return g_test_run ();
}

View File

@ -1,254 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
*/
#include "wsinstance.h"
#include <assert.h>
#include <err.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <gnutls/x509.h>
#include "common/cockpitmemory.h"
#include "utils.h"
/* This is a bit lame, but having a hard limit on peer certificates is
* desirable: Let's not get DoSed by huge certs */
#define MAX_PEER_CERT_SIZE 100000
__attribute__((__format__ (__printf__, 3, 4)))
static void
snprintf_checked (char *str,
size_t size,
const char *fmt, ...)
{
va_list args;
int r;
va_start (args, fmt);
r = vsnprintf (str, size, fmt, args);
va_end (args);
if (r >= size)
{
fprintf (stderr, "snprintf_checked got a too small buffer of %zu bytes but tried to print %i\n", size, r);
abort ();
}
}
static void
ws_write_peer_cert (WsInstance *ws, const char *cert_pem, size_t cert_pem_size)
{
char *tempname;
int fd;
int r;
asprintfx (&ws->peer_cert_file, "%s/ws.%u.crt", ws->state_dir, ws->pid);
asprintfx (&tempname, "%s.tmp", ws->peer_cert_file);
fd = open (tempname, O_CREAT | O_WRONLY | O_EXCL, 0644);
if (fd < 0)
err (1, "Could not open certificate file %s", tempname);
r = write (fd, cert_pem, cert_pem_size);
if (r < 0)
err (1, "Could not write certificate file");
if (r != cert_pem_size)
errx (1, "Could not write certificate file: wrote %i out of %zu bytes", r, cert_pem_size);
close (fd);
if (rename (tempname, ws->peer_cert_file) < 0)
err (1, "Could not rename %s", tempname);
free (tempname);
debug (SERVER, "Wrote TLS peer certificate PEM to %s", ws->peer_cert_file);
}
/**
* ws_init_peer_cert: Retrieve and publish information about the client-side TLS certificate
*/
static void
ws_init_peer_cert (WsInstance *ws, const gnutls_datum_t *der)
{
gnutls_x509_crt_t cert;
static char cert_pem[MAX_PEER_CERT_SIZE];
size_t cert_pem_size = sizeof (cert_pem);
assert (der);
assert (ws->pid);
/* clone DER certificate */
ws->peer_cert.size = der->size;
ws->peer_cert.data = mallocx (der->size);
memcpy (ws->peer_cert.data, der->data, der->size);
/* convert to X.509 to extract information and PEM */
gnutls_check (gnutls_x509_crt_init (&cert));
gnutls_check (gnutls_x509_crt_import (cert, der, GNUTLS_X509_FMT_DER));
gnutls_check (gnutls_x509_crt_print (cert, GNUTLS_CRT_PRINT_ONELINE, &ws->peer_cert_info));
gnutls_check (gnutls_x509_crt_export (cert, GNUTLS_X509_FMT_PEM, cert_pem, &cert_pem_size));
/* GnuTLS should already enforce that, but make double-sure */
assert (cert_pem_size < sizeof (cert_pem));
assert (cert_pem[cert_pem_size] == '\0');
debug (SERVER, "TLS peer certificate: %s", ws->peer_cert_info.data);
/* write X.509 certificate to our state dir, so that PAM modules can check that these got validated */
ws_write_peer_cert (ws, cert_pem, cert_pem_size);
gnutls_x509_crt_deinit (cert);
}
/**
* ws_instance_new: Launch a new cockpit-ws child process
*
* Sessions with different client TLS certificates, https-without-certificate,
* and unencrypted http get shielded from each other, so that attacks in one ws
* cannot tamper with other sessions.
*
* @ws_path: Path to cockpit-ws binary
* @mode: #WS_INSTANCE_{HTTP,HTTP_REDIRECT,HTTPS}
* @client_cert_der: client TLS certificate in DER format (as retrieved from gnutls_certificate_get_peers())
* @state_dir: Directory for putting the unix socket to cockpit-ws and
* certificate information. This is sensitive and must only be accessible to cockpit-tls!
*/
WsInstance *
ws_instance_new (const char *ws_path,
enum WsInstanceMode mode,
const gnutls_datum_t *client_cert_der,
const char *state_dir)
{
WsInstance *ws;
int fd;
pid_t pid;
static char pid_str[20];
ws = callocx (1, sizeof (WsInstance));
ws->state_dir = state_dir;
/* create a listening socket for cockpit-ws */
fd = socket (AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
err (1, "failed to create cockpit-ws socket");
ws->socket.sun_family = AF_UNIX;
snprintf_checked (ws->socket.sun_path, sizeof (ws->socket.sun_path), "%s/ws.%i.sock", state_dir, fd);
unlink (ws->socket.sun_path);
if (bind (fd, (const struct sockaddr *) &ws->socket, sizeof (ws->socket)) < 0)
err (1, "failed to bind cockpit-ws socket %s", ws->socket.sun_path);
if (listen (fd, 20) < 0)
err (1, "failed to set cockpit-ws socket to listen");
pid = fork ();
if (pid < 0)
err (1, "failed to fork");
if (pid > 0)
{
/* parent */
debug (SERVER, "forked cockpit-ws as pid %i on socket %s", pid, ws->socket.sun_path);
close (fd);
ws->pid = pid;
if (mode == WS_INSTANCE_HTTPS && client_cert_der)
ws_init_peer_cert (ws, client_cert_der);
return ws;
}
/* child */
/* pass the socket to ws like systemd activation does, see sd_listen_fds(3) */
if (dup2 (fd, SD_LISTEN_FDS_START) < 0)
err (1, "failed to dup socket fd");
snprintf_checked (pid_str, sizeof (pid_str), "%i", getpid ());
setenv ("LISTEN_FDS", "1", 1);
setenv ("LISTEN_PID", pid_str, 1);
debug (SERVER, "cockpit-ws child process: setup complete, executing %s", ws_path);
switch (mode)
{
case WS_INSTANCE_HTTP:
execl (ws_path, ws_path, "--no-tls", "--port", "0", NULL);
break;
case WS_INSTANCE_HTTP_REDIRECT:
execl (ws_path, ws_path, "--proxy-tls-redirect", "--no-tls", "--port", "0", NULL);
break;
case WS_INSTANCE_HTTPS:
execl (ws_path, ws_path, "--for-tls-proxy", "--port", "0", NULL);
break;
default:
errx (1, "Invalid mode");
}
err (127, "failed to execute %s", ws_path);
}
void
ws_instance_free (WsInstance *ws,
bool terminate)
{
debug (SERVER, "freeing cockpit-ws instance pid %i on socket %s", ws->pid, ws->socket.sun_path);
if (ws->peer_cert.size)
{
gnutls_free (ws->peer_cert.data);
gnutls_free (ws->peer_cert_info.data);
}
/* Only kill() and waitpid() the process in the case that we didn't
* already collect its exit status with waitpid(). This removes the
* theoretical possibility of calling kill() on an already-recycled
* pid.
*/
if (terminate)
{
kill (ws->pid, SIGKILL);
waitpid (ws->pid, NULL, 0);
}
unlink (ws->socket.sun_path);
if (ws->peer_cert_file)
{
unlink (ws->peer_cert_file);
free (ws->peer_cert_file);
}
free (ws);
}
/**
* ws_instance_has_peer_cert: Check if that instance is for a given GnuTLS DER client certificate
*
* Returns true if either this ws instance has no client certificate and der is
* %NULL or empty, or if both certificates are identical. Otherwise returns false.
*/
bool
ws_instance_has_peer_cert (WsInstance *ws, const gnutls_datum_t *der)
{
if (!der)
return ws->peer_cert.size == 0;
/* this includes the case where both are absent */
if (ws->peer_cert.size != der->size)
return false;
return memcmp (ws->peer_cert.data, der->data, der->size) == 0;
}

View File

@ -1,45 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdbool.h>
#include <sys/un.h>
#include <gnutls/gnutls.h>
enum WsInstanceMode { WS_INSTANCE_HTTP, WS_INSTANCE_HTTP_REDIRECT, WS_INSTANCE_HTTPS };
/* a single cockpit-ws child process */
typedef struct WsInstance {
const char *state_dir;
gnutls_datum_t peer_cert; /* DER format */
gnutls_datum_t peer_cert_info; /* human readable string */
char *peer_cert_file;
struct sockaddr_un socket;
pid_t pid;
struct WsInstance *next;
} WsInstance;
WsInstance* ws_instance_new (const char *ws_path,
enum WsInstanceMode mode,
const gnutls_datum_t *client_cert_der,
const char *state_dir);
void ws_instance_free (WsInstance *ws, bool terminate);
bool ws_instance_has_peer_cert (WsInstance *ws, const gnutls_datum_t *der);

View File

@ -164,6 +164,12 @@ remotectl_LDADD = \
nodist_systemdunit_DATA += \
src/ws/cockpit.socket \
src/ws/cockpit.service \
src/ws/cockpit-wsinstance-http.socket \
src/ws/cockpit-wsinstance-http.service \
src/ws/cockpit-wsinstance-http-redirect.socket \
src/ws/cockpit-wsinstance-http-redirect.service \
src/ws/cockpit-wsinstance-https.socket \
src/ws/cockpit-wsinstance-https.service \
src/ws/cockpit-motd.service \
$(NULL)
@ -191,7 +197,7 @@ $(nodist_tempconf_DATA): $(tempconf_in)
# If running cockpit-ws as a non-standard user, we also set up
# cockpit-session to be setuid root, but only runnable by cockpit-session
install-exec-hook::
chown -f root:$(COCKPIT_GROUP) $(DESTDIR)$(libexecdir)/cockpit-session || true
chown -f root:$(COCKPIT_WSINSTANCE_GROUP) $(DESTDIR)$(libexecdir)/cockpit-session || true
test "$(COCKPIT_USER)" != "root" && chmod -f 4750 $(DESTDIR)$(libexecdir)/cockpit-session || true
libexec_SCRIPTS = cockpit-desktop
@ -203,6 +209,12 @@ EXTRA_DIST += \
src/ws/cockpit-motd.service.in \
src/ws/cockpit.service.in \
src/ws/cockpit.socket.in \
src/ws/cockpit-wsinstance-http.service.in \
src/ws/cockpit-wsinstance-http.socket.in \
src/ws/cockpit-wsinstance-http-redirect.service.in \
src/ws/cockpit-wsinstance-http-redirect.socket.in \
src/ws/cockpit-wsinstance-https.service.in \
src/ws/cockpit-wsinstance-https.socket.in \
src/ws/cockpit-desktop.in \
$(firewall_DATA) \
$(tempconf_in) \
@ -211,6 +223,12 @@ EXTRA_DIST += \
CLEANFILES += \
src/ws/cockpit.socket \
src/ws/cockpit.service \
src/ws/cockpit-wsinstance-http.service \
src/ws/cockpit-wsinstance-http.socket \
src/ws/cockpit-wsinstance-http-redirect.service \
src/ws/cockpit-wsinstance-http-redirect.socket \
src/ws/cockpit-wsinstance-https.service \
src/ws/cockpit-wsinstance-https.socket \
src/ws/cockpit-motd.service \
$(nodist_tempconf_DATA) \
cockpit-desktop \

View File

@ -0,0 +1,9 @@
[Unit]
Description=Cockpit Web Service http-redirect instance
PartOf=cockpit.service
Documentation=man:cockpit-ws(8)
[Service]
ExecStart=@libexecdir@/cockpit-ws --proxy-tls-redirect --no-tls --port 0
User=@wsinstanceuser@
Group=@wsinstancegroup@

View File

@ -0,0 +1,9 @@
[Unit]
Description=Socket for Cockpit Web Service http-redirect instance
PartOf=cockpit.service
Documentation=man:cockpit-ws(8)
[Socket]
ListenStream=/run/cockpit/wsinstance/http-redirect.sock
SocketUser=@user@
SocketMode=0600

View File

@ -0,0 +1,9 @@
[Unit]
Description=Cockpit Web Service http instance
PartOf=cockpit.service
Documentation=man:cockpit-ws(8)
[Service]
ExecStart=@libexecdir@/cockpit-ws --no-tls --port=0
User=@wsinstanceuser@
Group=@wsinstancegroup@

View File

@ -0,0 +1,9 @@
[Unit]
Description=Socket for Cockpit Web Service http instance
PartOf=cockpit.service
Documentation=man:cockpit-ws(8)
[Socket]
ListenStream=/run/cockpit/wsinstance/http.sock
SocketUser=@user@
SocketMode=0600

View File

@ -0,0 +1,9 @@
[Unit]
Description=Cockpit Web Service https instance
PartOf=cockpit.service
Documentation=man:cockpit-ws(8)
[Service]
ExecStart=@libexecdir@/cockpit-ws --for-tls-proxy --port=0
User=@wsinstanceuser@
Group=@wsinstancegroup@

View File

@ -0,0 +1,9 @@
[Unit]
Description=Socket for Cockpit Web Service https instance
PartOf=cockpit.service
Documentation=man:cockpit-ws(8)
[Socket]
ListenStream=/run/cockpit/wsinstance/https.sock
SocketUser=@user@
SocketMode=0600

View File

@ -2,6 +2,8 @@
Description=Cockpit Web Service
Documentation=man:cockpit-ws(8)
Requires=cockpit.socket
Requires=cockpit-wsinstance-http.socket cockpit-wsinstance-http-redirect.socket cockpit-wsinstance-https.socket
After=cockpit-wsinstance-http.socket cockpit-wsinstance-http-redirect.socket cockpit-wsinstance-https.socket
[Service]
RuntimeDirectory=cockpit/tls

View File

@ -447,7 +447,7 @@ G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 --local
m.spawn("socat TCP-LISTEN:9091,reuseaddr,fork TCP:localhost:9099", "socat.log")
# ws with plain --no-tls should fail after login with mismatching Origin (expected http, got https)
m.spawn("su -s /bin/sh -c '%s --no-tls -p 9099' cockpit-ws" % self.ws_executable,
m.spawn("su -s /bin/sh -c '%s --no-tls -p 9099' cockpit-wsinstance" % self.ws_executable,
"ws-notls.log")
m.wait_for_cockpit_running(tls=True)
@ -488,7 +488,7 @@ G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 --local
self.allow_browser_errors("Error reading machine id")
# ws with --for-tls-proxy accepts only https origins, thus should work
m.spawn("su -s /bin/sh -c '%s --for-tls-proxy -p 9099 -a 127.0.0.1' cockpit-ws" % self.ws_executable,
m.spawn("su -s /bin/sh -c '%s --for-tls-proxy -p 9099 -a 127.0.0.1' cockpit-wsinstance" % self.ws_executable,
"ws-fortlsproxy.log")
m.wait_for_cockpit_running(tls=True)
b.open("https://%s:%s/system" % (b.address, b.port))
@ -520,7 +520,7 @@ G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 --local
# ws with --proxy-tls-redirect redirects non-localhost to https
m.execute("pkill -e cockpit-ws; while pgrep -a cockpit-ws; do sleep 1; done")
m.spawn("su -s /bin/sh -c '%s --proxy-tls-redirect --no-tls -p 9099 -a 127.0.0.1' cockpit-ws" % self.ws_executable,
m.spawn("su -s /bin/sh -c '%s --proxy-tls-redirect --no-tls -p 9099 -a 127.0.0.1' cockpit-wsinstance" % self.ws_executable,
"ws-proxy-tls-redirect.log")
m.wait_for_cockpit_running(tls=True)
self.assertIn("HTTP/1.1 301 Moved Permanently", m.execute("curl --silent --head http://172.27.0.15:9091"))

View File

@ -124,6 +124,7 @@ exec 2>&1
%configure \
--disable-silent-rules \
--with-cockpit-user=cockpit-ws \
--with-cockpit-ws-instance-user=cockpit-wsinstance \
--with-selinux-config-type=etc_t \
--with-appstream-data-packages='[ "appstream-data" ]' \
--with-nfs-client-package='"nfs-utils"' \
@ -412,20 +413,28 @@ The Cockpit Web Service listens on the network, and authenticates users.
%{_unitdir}/cockpit.service
%{_unitdir}/cockpit-motd.service
%{_unitdir}/cockpit.socket
%{_unitdir}/cockpit-wsinstance-http.socket
%{_unitdir}/cockpit-wsinstance-http.service
%{_unitdir}/cockpit-wsinstance-http-redirect.socket
%{_unitdir}/cockpit-wsinstance-http-redirect.service
%{_unitdir}/cockpit-wsinstance-https.socket
%{_unitdir}/cockpit-wsinstance-https.service
%{_prefix}/%{__lib}/tmpfiles.d/cockpit-tempfiles.conf
%{_sbindir}/remotectl
%{_libdir}/security/pam_ssh_add.so
%{_libexecdir}/cockpit-ws
%{_libexecdir}/cockpit-tls
%{_libexecdir}/cockpit-desktop
%attr(4750, root, cockpit-ws) %{_libexecdir}/cockpit-session
%attr(4750, root, cockpit-wsinstance) %{_libexecdir}/cockpit-session
%attr(775, -, wheel) %{_localstatedir}/lib/cockpit
%{_datadir}/cockpit/static
%{_datadir}/cockpit/branding
%pre ws
getent group cockpit-ws >/dev/null || groupadd -r cockpit-ws
getent passwd cockpit-ws >/dev/null || useradd -r -g cockpit-ws -d /nonexisting -s /sbin/nologin -c "User for cockpit-ws" cockpit-ws
getent passwd cockpit-ws >/dev/null || useradd -r -g cockpit-ws -d /nonexisting -s /sbin/nologin -c "User for cockpit web service" cockpit-ws
getent group cockpit-wsinstance >/dev/null || groupadd -r cockpit-wsinstance
getent passwd cockpit-wsinstance >/dev/null || useradd -r -g cockpit-wsinstance -d /nonexisting -s /sbin/nologin -c "User for cockpit-ws instances" cockpit-wsinstance
%post ws
%systemd_post cockpit.socket

View File

@ -5,6 +5,12 @@ etc/pam.d/cockpit
lib/systemd/system/cockpit.service
lib/systemd/system/cockpit-motd.service
lib/systemd/system/cockpit.socket
lib/systemd/system/cockpit-wsinstance-http-redirect.service
lib/systemd/system/cockpit-wsinstance-http-redirect.socket
lib/systemd/system/cockpit-wsinstance-http.service
lib/systemd/system/cockpit-wsinstance-http.socket
lib/systemd/system/cockpit-wsinstance-https.service
lib/systemd/system/cockpit-wsinstance-https.socket
lib/*/security/pam_ssh_add.so
usr/lib/tmpfiles.d/cockpit-tempfiles.conf
usr/lib/cockpit/cockpit-session

View File

@ -2,9 +2,16 @@
set -e
adduser --system --group --home /nonexisting --no-create-home cockpit-ws
adduser --system --group --home /nonexisting --no-create-home cockpit-wsinstance
# change group of cockpit-session on upgrades (changed in version 203)
if OUT=$(dpkg-statoverride --list /usr/lib/cockpit/cockpit-session) && [ "$OUT#root cockpit-ws 4750}" != "$OUT" ]; then
echo "Adjusting /usr/lib/cockpit/cockpit-session permissions..."
dpkg-statoverride --remove /usr/lib/cockpit/cockpit-session
fi
if ! dpkg-statoverride --list /usr/lib/cockpit/cockpit-session >/dev/null; then
dpkg-statoverride --update --add root cockpit-ws 4750 /usr/lib/cockpit/cockpit-session
dpkg-statoverride --update --add root cockpit-wsinstance 4750 /usr/lib/cockpit/cockpit-session
fi
#DEBHELPER#

View File

@ -21,6 +21,7 @@ override_dh_auto_configure:
dh_auto_configure -- \
--with-networkmanager-needs-root=yes \
--with-cockpit-user=cockpit-ws \
--with-cockpit-ws-instance-user=cockpit-wsinstance \
--with-appstream-config-packages='[ "appstream" ]' \
--with-nfs-client-packages='"nfs-common"' \
--with-pamdir=/lib/$(DEB_HOST_MULTIARCH)/security \