367 lines
9.4 KiB
C
367 lines
9.4 KiB
C
/*
|
|
* 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/>.
|
|
*/
|
|
|
|
/* for secure_getenv () */
|
|
#define _GNU_SOURCE
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include "server.h"
|
|
|
|
#include <assert.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <netinet/in.h>
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <sys/epoll.h>
|
|
#include <sys/param.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/timerfd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
#include "connection.h"
|
|
#include "utils.h"
|
|
|
|
/* cockpit-tls TCP server state (singleton) */
|
|
static struct {
|
|
/* only used from main thread */
|
|
bool initialized;
|
|
int first_listener;
|
|
int last_listener;
|
|
int epollfd;
|
|
|
|
/* rw, protected by mutex */
|
|
pthread_mutex_t connection_mutex;
|
|
unsigned int connection_count;
|
|
int idle_timerfd;
|
|
struct itimerspec idle_timeout;
|
|
} server;
|
|
|
|
/**
|
|
* check_sd_listen_pid: Verify that systemd-activated socket is for us
|
|
*
|
|
* See sd_listen_fds(3).
|
|
*/
|
|
static bool
|
|
check_sd_listen_pid (void)
|
|
{
|
|
const char *pid_str = secure_getenv ("LISTEN_PID");
|
|
long pid;
|
|
char *endptr = NULL;
|
|
|
|
if (!pid_str)
|
|
{
|
|
warnx ("$LISTEN_PID not set, not accepting socket activation");
|
|
return false;
|
|
}
|
|
|
|
pid = strtol (pid_str, &endptr, 10);
|
|
if (pid <= 0 || *endptr != '\0')
|
|
errx (1, "$LISTEN_PID contains invalid value '%s'", pid_str);
|
|
if ((pid_t) pid != getpid ())
|
|
{
|
|
warnx ("$LISTEN_PID %li is not for us, ignoring", pid);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void *
|
|
server_connection_thread_start_routine (void *data)
|
|
{
|
|
int fd = (uintptr_t) data;
|
|
|
|
connection_thread_main (fd);
|
|
|
|
/* teardown */
|
|
{
|
|
pthread_mutex_lock (&server.connection_mutex);
|
|
|
|
server.connection_count--;
|
|
|
|
debug (CONNECTION, "Server.connection_count decreased to %i", server.connection_count);
|
|
|
|
if (server.connection_count == 0 && server.idle_timerfd != -1)
|
|
{
|
|
debug (CONNECTION, " -> setting idle timeout");
|
|
timerfd_settime (server.idle_timerfd, 0, &server.idle_timeout, NULL);
|
|
}
|
|
|
|
pthread_mutex_unlock (&server.connection_mutex);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* handle_accept: Handle event on listening fd
|
|
*
|
|
* I. e. accepting new connections
|
|
*/
|
|
static void
|
|
handle_accept (int listen_fd)
|
|
{
|
|
int fd;
|
|
pthread_attr_t attr;
|
|
pthread_t thread;
|
|
|
|
debug (CONNECTION, "epoll_wait event on server listen fd %i", listen_fd);
|
|
|
|
/* accept and create new connection */
|
|
fd = accept4 (listen_fd, NULL, NULL, SOCK_CLOEXEC);
|
|
if (fd < 0)
|
|
{
|
|
if (errno != EINTR)
|
|
warn ("failed to accept connection");
|
|
return;
|
|
}
|
|
|
|
debug (CONNECTION, "New connection accepted, fd %i", fd);
|
|
|
|
{
|
|
pthread_mutex_lock (&server.connection_mutex);
|
|
|
|
if (server.connection_count == 0 && server.idle_timerfd != -1)
|
|
{
|
|
const struct itimerspec zero = { { 0 }, };
|
|
debug (CONNECTION, " -> clearing idle timeout.");
|
|
timerfd_settime (server.idle_timerfd, 0, &zero, NULL);
|
|
}
|
|
|
|
server.connection_count++;
|
|
|
|
debug (CONNECTION, " -> server.connection_count is now %i", server.connection_count);
|
|
|
|
pthread_mutex_unlock (&server.connection_mutex);
|
|
}
|
|
|
|
pthread_attr_init (&attr);
|
|
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
|
|
|
|
int r = pthread_create (&thread, &attr,
|
|
server_connection_thread_start_routine,
|
|
(void *) (uintptr_t) fd);
|
|
|
|
if (r != 0)
|
|
{
|
|
errno = r;
|
|
warn ("pthread_create() failed. dropping connection");
|
|
close (fd);
|
|
}
|
|
|
|
pthread_attr_destroy (&attr);
|
|
}
|
|
|
|
/***********************************
|
|
*
|
|
* Public API
|
|
*
|
|
***********************************/
|
|
|
|
/**
|
|
* server_init: Initialize cockpit TLS proxy server
|
|
*
|
|
* There is only one instance of this. Trying to initialize it more than once
|
|
* is an error.
|
|
*
|
|
* @ws_path: Path to cockpit-wsinstance sockets directory
|
|
* @idle_timeout: When positive, stop server after given number of seconds with
|
|
* no connections
|
|
* @port: Port to listen to; ignored when the listening socket is handed over
|
|
* through the systemd socket activation protocol
|
|
*/
|
|
void
|
|
server_init (const char *wsinstance_sockdir,
|
|
int idle_timeout,
|
|
uint16_t port)
|
|
{
|
|
const char *env_listen_fds;
|
|
struct epoll_event ev = { .events = EPOLLIN };
|
|
|
|
assert (!server.initialized);
|
|
server.initialized = true;
|
|
|
|
connection_set_wsinstance_sockdir (wsinstance_sockdir);
|
|
|
|
pthread_mutex_init (&server.connection_mutex, NULL);
|
|
|
|
/* systemd socket activated? */
|
|
env_listen_fds = secure_getenv ("LISTEN_FDS");
|
|
if (env_listen_fds && check_sd_listen_pid ())
|
|
{
|
|
char *endptr = NULL;
|
|
unsigned long n = strtoul (env_listen_fds, &endptr, 10);
|
|
|
|
if (n < 1 || n > INT_MAX || *endptr != '\0')
|
|
errx (1, "Invalid $LISTEN_FDS value '%s'", env_listen_fds);
|
|
|
|
server.first_listener = SD_LISTEN_FDS_START;
|
|
server.last_listener = SD_LISTEN_FDS_START + (n - 1);
|
|
}
|
|
else
|
|
{
|
|
struct sockaddr_in sa_serv;
|
|
int optval = 1;
|
|
|
|
/* Listen to our port; on the command line and our API we just support one */
|
|
server.first_listener = socket (AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
|
if (server.first_listener < 0)
|
|
err (1, "failed to create server listening fd");
|
|
server.last_listener = server.first_listener;
|
|
|
|
memset (&sa_serv, '\0', sizeof (sa_serv));
|
|
sa_serv.sin_family = AF_INET;
|
|
sa_serv.sin_addr.s_addr = INADDR_ANY;
|
|
sa_serv.sin_port = htons (port);
|
|
|
|
if (setsockopt (server.first_listener, SOL_SOCKET, SO_REUSEADDR, (void *) &optval, sizeof (int)) < 0)
|
|
err (1, "failed to set socket option");
|
|
if (bind (server.first_listener, (struct sockaddr *) &sa_serv, sizeof (sa_serv)) < 0)
|
|
err (1, "failed to bind to port %hu", port);
|
|
if (listen (server.first_listener, 1024) < 0)
|
|
err (1, "failed to listen to server port");
|
|
debug (SERVER, "Server ready. Listening on port %hu, fd %i", port, server.first_listener);
|
|
}
|
|
|
|
/* epoll the listening fds */
|
|
server.epollfd = epoll_create1 (EPOLL_CLOEXEC);
|
|
if (server.epollfd < 0)
|
|
err (1, "Failed to create epoll fd");
|
|
for (int fd = server.first_listener; fd <= server.last_listener; fd++)
|
|
{
|
|
ev.data.fd = fd;
|
|
if (epoll_ctl (server.epollfd, EPOLL_CTL_ADD, fd, &ev) < 0)
|
|
err (1, "Failed to epoll server listening fd");
|
|
}
|
|
|
|
/* we use timerfd for idle timeout. epoll that too. */
|
|
if (idle_timeout > 0)
|
|
{
|
|
server.idle_timerfd = timerfd_create (CLOCK_MONOTONIC, TFD_CLOEXEC);
|
|
if (server.idle_timerfd == -1)
|
|
err (1, "Failed to create timerfd");
|
|
|
|
server.idle_timeout.it_value.tv_sec = idle_timeout;
|
|
if (timerfd_settime (server.idle_timerfd, 0, &server.idle_timeout, NULL) != 0)
|
|
err (1, "Failed to set timerfd");
|
|
|
|
ev.data.fd = server.idle_timerfd;
|
|
if (epoll_ctl (server.epollfd, EPOLL_CTL_ADD, server.idle_timerfd, &ev) < 0)
|
|
err (1, "Failed to epoll idle timerfd");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* server_cleanup: Free all resources to the cockpit TLS proxy server
|
|
*
|
|
* There is only one instance of this. Trying to free it more than once
|
|
* is an error.
|
|
*/
|
|
void
|
|
server_cleanup (void)
|
|
{
|
|
assert (server.initialized);
|
|
assert (server.connection_count == 0);
|
|
|
|
if (server.idle_timerfd != -1)
|
|
close (server.idle_timerfd);
|
|
|
|
for (int fd = server.first_listener; fd <= server.last_listener; fd++)
|
|
close (fd);
|
|
|
|
close (server.epollfd);
|
|
|
|
pthread_mutex_destroy (&server.connection_mutex);
|
|
|
|
connection_cleanup ();
|
|
|
|
memset (&server, 0, sizeof server);
|
|
}
|
|
|
|
/**
|
|
* server_poll_event: Wait for and process one event
|
|
*
|
|
* @timeout: number of milliseconds to wait for an event to happen; after that,
|
|
* the function will return false. -1 will to block until an event occurs.
|
|
*
|
|
* This can be an event on a listening socket, or the idle timeout if no
|
|
* clients are connected.
|
|
*
|
|
* Returns: false on timeout, true if some (other) event was handled.
|
|
*/
|
|
bool
|
|
server_poll_event (int timeout)
|
|
{
|
|
int ret;
|
|
struct epoll_event ev;
|
|
|
|
assert (server.initialized);
|
|
|
|
ret = epoll_wait (server.epollfd, &ev, 1, timeout);
|
|
if (ret == 0)
|
|
return false; /* hit timeout */
|
|
|
|
if (ret == 1)
|
|
{
|
|
int fd = ev.data.fd;
|
|
|
|
if (fd == server.idle_timerfd)
|
|
return false; /* hit the other timeout */
|
|
|
|
assert (server.first_listener <= fd && fd <= server.last_listener);
|
|
|
|
handle_accept (fd);
|
|
}
|
|
else if (errno != EINTR)
|
|
err (1, "Failed to epoll_wait");
|
|
|
|
return true; /* did something */
|
|
}
|
|
|
|
/**
|
|
* server_run: Server main loop
|
|
*
|
|
* Returns if the server reached the idle timeout, otherwise runs forever.
|
|
*/
|
|
void
|
|
server_run (void)
|
|
{
|
|
while (server_poll_event (-1))
|
|
;
|
|
}
|
|
|
|
unsigned
|
|
server_num_connections (void)
|
|
{
|
|
unsigned count;
|
|
|
|
pthread_mutex_lock (&server.connection_mutex);
|
|
count = server.connection_count;
|
|
pthread_mutex_unlock (&server.connection_mutex);
|
|
|
|
return count;
|
|
}
|