* Copyright (C) 2013 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "config.h"
#include "session-utils.h"
#include "common/cockpitframe.h"
#include "common/cockpitjsonprint.h"
#include "common/cockpitmemory.h"
#include <fcntl.h>
#include <paths.h>
#include <stdarg.h>
#include <stdlib.h>
#include <sys/param.h>
#include <time.h>
#include <utmp.h>
#ifndef _PATH_BTMP
#define _PATH_BTMP "/var/log/btmp"
#endif
const char *program_name;
struct passwd *pwd;
pid_t child;
int want_session = 1;
static char *auth_prefix = NULL;
static size_t auth_prefix_size = 0;
static char *auth_msg = NULL;
static size_t auth_msg_size = 0;
static FILE *authf = NULL;
char *
read_authorize_response (const char *what)
{
unsigned char *message;
debug ("reading %s authorize message", what);
ssize_t len = cockpit_frame_read (STDIN_FILENO, &message);
if (len < 0)
err (EX, "couldn't read %s", what);
if (len == 0)
{
debug ("EOF reading %s authorize message", what);
exit (0);
}
return (char *)message;
}
char *get_authorize_key (const char *json, const char *key, bool required)
{
* The authorize messages we receive always have this structure:
*
* \n{"command":"authorize","cookie":"NNN","response":"...",...more entries...}
*
* We make many assumptions/restrictions here to keep the parser simple. In particular, no extra
* whitespace, everything is a string, and no escaped ".
*/
if (json == NULL || json[0] != '\n' || json[1] != '{')
errx (EX, "authorize message doesn't start with \\n{");
char *key_match;
int key_match_len = asprintfx (&key_match, "\"%s\":\"", key);
const char *match = strstr (json, key_match);
free (key_match);
if (!match)
{
if (required)
errx (EX, "authorize message missing required key %s", key);
debug ("authorize message missing optional key %s", key);
return NULL;
}
match += key_match_len;
size_t value_len = strcspn (match, "\"");
if (match[value_len] != '"')
errx (EX, "syntax error, expected closing quote");
return strndup (match, value_len);
}
void
write_control_string (const char *field,
const char *str)
{
cockpit_json_print_string_property (authf, field, str, -1);
}
void
write_control_bool (const char *field,
bool val)
{
cockpit_json_print_bool_property (authf, field, val);
}
void
write_authorize_begin (void)
{
assert (authf == NULL);
assert (auth_msg_size == 0);
assert (auth_msg == NULL);
debug ("writing auth challenge");
if (auth_prefix)
{
free (auth_prefix);
auth_prefix = NULL;
}
if (asprintf (&auth_prefix, "\n{\"command\":\"authorize\",\"cookie\":\"session%u%u\"",
(unsigned int)getpid(), (unsigned int)time (NULL)) < 0)
{
errx (EX, "out of memory allocating string");
}
auth_prefix_size = strlen (auth_prefix);
authf = open_memstream (&auth_msg, &auth_msg_size);
if (!authf)
err (EX, "failed to open_memstream()");
fprintf (authf, "%s", auth_prefix);
}
void
write_control_end (void)
{
assert (authf != NULL);
fprintf (authf, "}\n");
fflush (authf);
fclose (authf);
assert (auth_msg_size > 0);
assert (auth_msg != NULL);
if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)auth_msg, auth_msg_size) < 0)
err (EX, "couldn't write auth request");
debug ("finished auth request");
free (auth_msg);
auth_msg = NULL;
authf = NULL;
auth_msg_size = 0;
}
__attribute__((__noreturn__)) void
exit_init_problem (const char *problem, const char *message)
{
char *payload = NULL;
size_t payload_size = 0;
debug ("writing init problem %s message %s", problem, message);
FILE *stream = open_memstream (&payload, &payload_size);
if (!stream)
errx (EX, "couldn't allocate memory for message");
fprintf (stream, "\n{\"command\":\"init\",\"version\":1");
cockpit_json_print_string_property (stream, "problem", problem, -1);
cockpit_json_print_string_property (stream, "message", message, -1);
fprintf (stream, "}");
fclose (stream);
if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)payload, payload_size) < 0)
err (EX, "couldn't write init message");
free (payload);
exit (5);
}
void
build_string (char **buf,
size_t *size,
const char *str,
size_t len)
{
if (*size == 0)
return;
if (len > *size - 1)
len = *size - 1;
memcpy (*buf, str, len);
(*buf)[len] = '\0';
*buf += len;
*size -= len;
}
static bool
do_lastlog (uid_t uid,
const struct timeval *now,
const char *rhost,
time_t *out_last_login,
FILE *messages)
{
struct lastlog entry;
bool result = false;
int fd = -1;
ssize_t r;
fd = open (_PATH_LASTLOG, O_RDWR);
if (fd == -1)
{
warn ("failed to open %s", _PATH_LASTLOG);
goto out;
}
r = pread (fd, &entry, sizeof entry, uid * sizeof entry);
if (r == sizeof entry && entry.ll_time != 0)
{
* can also extend to the full length of the field without
* nul-termination. use the maxlen parameter to help with that.
*/
if (!cockpit_json_print_integer_property (messages, "last-login-time", entry.ll_time) ||
!cockpit_json_print_string_property (messages, "last-login-host", entry.ll_host, UT_HOSTSIZE) ||
!cockpit_json_print_string_property (messages, "last-login-line", entry.ll_line, UT_LINESIZE))
{
warnx ("failed to print last-login details to messages memfd");
goto out;
}
if (out_last_login)
*out_last_login = entry.ll_time;
}
else if (r == sizeof entry)
{
*out_last_login = 0;
}
else if (r == 0)
{
*out_last_login = 0;
}
else if (r < 0)
{
warn ("failed to pread() %s for uid %u", _PATH_LASTLOG, (unsigned) uid);
goto out;
}
else
{
warnx ("incomplete pread() %s for uid %u: %zu of %zu bytes",
_PATH_LASTLOG, (unsigned) uid, r, sizeof entry);
goto out;
}
* designed for what we need to do: copy a string up to N characters
* into a fixed width field, adding nul bytes if the string is shorter
* than N.
*
* Unfortunately, when you use it in this way, GCC is convinced that
* you don't know what you're doing and gives a warning that's very
* difficult to get rid of. We tried using #pragma here before, but
* after several attempts, it was difficult to get the
* conditionalising (for the compiler version) correct.
*
* Let's just nul out the struct and use memcpy(). Sigh.
*
* strncpy (entry.ll_host, rhost, sizeof entry.ll_host);
* strncpy (entry.ll_line, "web console", sizeof entry.ll_line);
*
* See also https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94615
* See also https://sourceware.org/bugzilla/show_bug.cgi?id=25844
*/
memset (&entry, 0, sizeof entry);
memcpy (entry.ll_host, rhost, MIN (strlen (rhost), sizeof entry.ll_host));
const char * const line = "web console";
memcpy (entry.ll_line, line, MIN (strlen (line), sizeof entry.ll_line));
entry.ll_time = now->tv_sec;
r = pwrite (fd, &entry, sizeof entry, uid * sizeof entry);
if (r == -1)
{
warn ("failed to pwrite() %s for uid %u", _PATH_LASTLOG, (unsigned) uid);
goto out;
}
else if (r != sizeof entry)
{
warnx ("incomplete pwrite() %s for uid %u: %zu or %zu bytes",
_PATH_LASTLOG, (unsigned) uid, r, sizeof entry);
goto out;
}
result = true;
out:
if (fd != -1)
close (fd);
return result;
}
static bool
scan_btmp (const char *username,
time_t last_success,
FILE *messages)
{
bool success = false;
int fail_count = 0;
struct utmp last;
int fd;
fd = open (_PATH_BTMP, O_RDONLY | O_CLOEXEC);
if (fd == -1)
{
if (errno == ENOENT)
{
success = true;
goto out;
}
warn ("open(%s) failed", _PATH_BTMP);
goto out;
}
while (true)
{
struct utmp entry;
ssize_t r;
do
r = read (fd, &entry, sizeof entry);
while (r == -1 && errno == EINTR);
if (r == 0)
break;
if (r < 0)
{
warn ("read(%s) failed", _PATH_BTMP);
goto out;
}
if (r != sizeof entry)
{
warnx ("read(%s) returned partial result (%zu of %zu bytes)",
_PATH_BTMP, r, sizeof entry);
goto out;
}
if (entry.ut_tv.tv_sec > last_success &&
strncmp (entry.ut_user, username, sizeof entry.ut_user) == 0)
{
last = entry;
fail_count++;
}
}
if (fail_count == 0)
{
success = true;
goto out;
}
success = cockpit_json_print_integer_property (messages, "fail-count", fail_count) &&
cockpit_json_print_integer_property (messages, "last-fail-time", last.ut_tv.tv_sec) &&
cockpit_json_print_string_property (messages, "last-fail-host", last.ut_host, UT_HOSTSIZE) &&
cockpit_json_print_string_property (messages, "last-fail-line", last.ut_line, UT_LINESIZE);
out:
if (fd > -1)
close (fd);
return success;
}
void
utmp_log (int login,
const char *rhost,
FILE *messages)
{
char id[UT_LINESIZE + 1];
struct utmp ut;
struct timeval tv;
int pid;
pid = getpid ();
snprintf (id, UT_LINESIZE, "%d", pid);
assert (pwd != NULL);
utmpname (_PATH_UTMP);
setutent ();
memset (&ut, 0, sizeof(ut));
strncpy (ut.ut_id, id, sizeof (ut.ut_id));
ut.ut_id[sizeof (ut.ut_id) - 1] = 0;
strncpy (ut.ut_line, "web console", sizeof ut.ut_line);
ut.ut_line[sizeof ut.ut_line - 1] = 0;
if (login)
{
strncpy (ut.ut_user, pwd->pw_name, sizeof(ut.ut_user));
ut.ut_user[sizeof (ut.ut_user) - 1] = 0;
strncpy (ut.ut_host, rhost, sizeof(ut.ut_host));
ut.ut_host[sizeof (ut.ut_host) - 1] = 0;
}
gettimeofday (&tv, NULL);
ut.ut_tv.tv_sec = tv.tv_sec;
ut.ut_tv.tv_usec = tv.tv_usec;
ut.ut_type = login ? USER_PROCESS : DEAD_PROCESS;
ut.ut_pid = pid;
pututline (&ut);
endutent ();
updwtmp (_PATH_WTMP, &ut);
if (login)
{
time_t last_success;
if (do_lastlog (pwd->pw_uid, &tv, rhost, &last_success, messages))
scan_btmp (pwd->pw_name, last_success, messages);
}
}
void
btmp_log (const char *username,
const char *rhost)
{
struct timeval tv;
* we need to read into a temporary variable and then copy the fields.
*/
gettimeofday (&tv, NULL);
struct utmp entry = {
.ut_line = "web console",
.ut_pid = getpid (),
.ut_tv.tv_sec = tv.tv_sec,
.ut_tv.tv_usec = tv.tv_usec,
.ut_type = LOGIN_PROCESS,
};
strncpy (entry.ut_host, rhost, sizeof entry.ut_host);
strncpy (entry.ut_user, username, sizeof entry.ut_user);
int fd = open (_PATH_BTMP, O_WRONLY | O_APPEND);
if (fd == -1)
{
warn ("open(%s) failed", _PATH_BTMP);
goto out;
}
ssize_t r = write (fd, &entry, sizeof entry);
if (r < 0)
{
warn ("write() %s failed", _PATH_BTMP);
goto out;
}
else if (r != sizeof entry)
{
warnx ("incomplete write() %s: %zu of %zu bytes",
_PATH_BTMP, r, sizeof entry);
goto out;
}
out:
if (fd != -1)
close (fd);
}
void
authorize_logger (const char *data)
{
warnx ("%s", data);
}
* to stderr and abort execution. Never returns.
*/
static noreturn void
__attribute__ ((format (printf, 1, 2)))
abort_with_message (const char *format,
...)
{
char buffer[1024];
va_list ap;
va_start (ap, format);
size_t length = vsnprintf (buffer, sizeof buffer, format, ap);
va_end (ap);
size_t ofs = 0;
while (ofs != length)
{
ssize_t r;
do
r = write (STDERR_FILENO, buffer + ofs, length - ofs);
while (r == -1 && errno == EINTR);
if (0 <= r && r <= length - ofs)
ofs += r;
else
break;
}
abort ();
}
* according to a specified array. All other file descriptors are
* closed.
*
* Commonly used after fork() and before exec().
*/
static void
fd_remap (const int *remap_fds,
int n_remap_fds)
{
if (n_remap_fds < 0 || n_remap_fds > 1024)
abort_with_message ("requested to fd_remap() too many fds!");
int *fds = alloca (sizeof (int) * n_remap_fds);
memcpy (fds, remap_fds, sizeof (int) * n_remap_fds);
* n_remap_fds in order to make sure that we don't overwrite them in
* the middle of the dup2() loop below, and also avoid the case that
* dup2() is a no-op (which could fail to clear the O_CLOEXEC flag,
* for example).
*/
for (int i = 0; i < n_remap_fds; i++)
if (fds[i] != -1 && fds[i] < n_remap_fds)
{
int new_fd = fcntl (fds[i], F_DUPFD, n_remap_fds);
if (new_fd == -1)
abort_with_message ("fcntl(%d, F_DUPFD) failed: %m", fds[i]);
fds[i] = new_fd;
}
for (int i = 0; i < n_remap_fds; i++)
if (fds[i] != -1)
if (dup2 (fds[i], i) != i)
abort_with_message ("dup2(%d, %d) failed: %m", fds[i], i);
close_range (n_remap_fds, ~0U, 0);
}
int
spawn_and_wait (const char **argv, const char **envp,
const int *remap_fds, int n_remap_fds,
uid_t uid, gid_t gid)
{
pid_t child;
child = fork ();
if (child == -1)
abort_with_message ("cockpit-session: fork() failed: %m");
if (child == 0)
{
if (setresgid (gid, gid, gid) != 0)
abort_with_message ("setresgid: couldn't set gid to %u: %m\n", (int) gid);
if (setresuid (uid, uid, uid) != 0)
abort_with_message ("setresuid: couldn't set uid to %u: %m\n", (int) uid);
{
uid_t real, effective, saved;
int r;
r = getresuid (&real, &effective, &saved);
assert (r == 0 && real == uid && effective == uid && saved == uid);
}
{
gid_t real, effective, saved;
int r;
r = getresgid (&real, &effective, &saved);
assert (r == 0 && real == gid && effective == gid && saved == gid);
}
if (n_remap_fds != -1)
fd_remap (remap_fds, n_remap_fds);
execvpe (argv[0], (char **) argv, (char **) envp);
_exit(127);
}
else
{
int wstatus;
int r;
do
r = waitpid (child, &wstatus, 0);
while (r == -1 && errno == EINTR);
if (r == -1)
abort_with_message ("waitpid(%d) on cockpit-bridge process failed: %m", (int) child);
assert (r == child);
return wstatus;
}
}