diff options
author | Russ Allbery <rra@dropbox.com> | 2016-07-26 17:35:14 -0700 |
---|---|---|
committer | Russ Allbery <rra@dropbox.com> | 2016-07-26 17:35:14 -0700 |
commit | 36b8178f28448afccfa0535396095bc72da3e0d4 (patch) | |
tree | 2aa27d1c8b55fc275b8a1ef38689d0c05b169de7 /server | |
parent | 7a1647ebc8e672cb826dd4ae91adb434d7d13aa5 (diff) |
Initial refactoring for ssh shell support
The first step towards adding a new server mode that can be run as
a shell via ssh. Refactor the code to more cleanly separate the
protocol implementation and the GSS-API and non-GSS-API bits, and
add the remctl-shell binary. This is currently entirely untested,
apart from ensuring that it doesn't break the existing server
implementation.
Diffstat (limited to 'server')
-rw-r--r-- | server/commands.c | 59 | ||||
-rw-r--r-- | server/event-util.c | 55 | ||||
-rw-r--r-- | server/generic.c | 63 | ||||
-rw-r--r-- | server/internal.h | 33 | ||||
-rw-r--r-- | server/process.c | 144 | ||||
-rw-r--r-- | server/remctl-shell.c | 153 | ||||
-rw-r--r-- | server/remctld.c | 39 | ||||
-rw-r--r-- | server/server-ssh.c | 281 | ||||
-rw-r--r-- | server/server-v1.c | 101 | ||||
-rw-r--r-- | server/server-v2.c | 76 |
10 files changed, 758 insertions, 246 deletions
diff --git a/server/commands.c b/server/commands.c index 36ec950..7815c42 100644 --- a/server/commands.c +++ b/server/commands.c @@ -6,6 +6,7 @@ * * Written by Russ Allbery <eagle@eyrie.org> * Based on work by Anton Ushakov + * Copyright 2016 Dropbox, Inc. * Copyright 2015 Russ Allbery <eagle@eyrie.org> * Copyright 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, * 2014 The Board of Trustees of the Leland Stanford Junior University @@ -161,15 +162,12 @@ server_send_summary(struct client *client, struct config *config) status_all = (int) WEXITSTATUS(process.status); else status_all = -1; - if (ok_any) { - if (client->protocol == 1) - server_v1_send_output(client, output, status_all); - else - server_v2_send_status(client, status_all); - } else { + if (ok_any) + client->finish(client, output, status_all); + else { notice("summary request from user %s, but no defined summaries", client->user); - server_send_error(client, ERROR_UNKNOWN_COMMAND, "Unknown command"); + client->error(client, ERROR_UNKNOWN_COMMAND, "Unknown command"); } if (output != NULL) evbuffer_free(output); @@ -278,21 +276,16 @@ create_argv_help(const char *path, const char *command, const char *subcommand) * Process an incoming command. Check the configuration files and the ACL * file, and if appropriate, forks off the command. Takes the argument vector * and the user principal, and a buffer into which to put the output from the - * executable or any error message. Returns 0 on success and a negative - * integer on failure. + * executable or any error message. Returns the exit status of the command. * * Using the command and the subcommand, the following argument, a lookup in - * the conf data structure is done to find the command executable and acl - * file. If the conf file, and subsequently the conf data structure contains - * an entry for this command with subcommand equal to "ALL", that is a - * wildcard match for any given subcommand. The first argument is then - * replaced with the actual program name to be executed. - * - * After checking the acl permissions, the process forks and the child execv's - * the command with pipes arranged to gather output. The parent waits for the - * return code and gathers stdout and stderr pipes. + * the configuration data structure is done to find the command executable and + * ACL file. If the configuration contains an entry for this command with + * subcommand equal to "ALL", that is a wildcard match for any given + * subcommand. The first argument is then replaced with the actual program + * name to be executed. */ -void +int server_run_command(struct client *client, struct config *config, struct iovec **argv) { @@ -302,6 +295,7 @@ server_run_command(struct client *client, struct config *config, struct rule *rule = NULL; char **req_argv = NULL; size_t i; + int status = -1; bool ok = false; bool help = false; const char *user = client->user; @@ -317,7 +311,7 @@ server_run_command(struct client *client, struct config *config, */ if (argv[0] == NULL) { notice("empty command from user %s", user); - server_send_error(client, ERROR_BAD_COMMAND, "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); goto done; } @@ -326,8 +320,7 @@ server_run_command(struct client *client, struct config *config, if (memchr(argv[i]->iov_base, '\0', argv[i]->iov_len)) { notice("%s from user %s contains nul octet", (i == 0) ? "command" : "subcommand", user); - server_send_error(client, ERROR_BAD_COMMAND, - "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); goto done; } } @@ -351,8 +344,8 @@ server_run_command(struct client *client, struct config *config, if (argv[1] != NULL && argv[2] != NULL && argv[3] != NULL) { notice("help command from user %s has more than three arguments", user); - server_send_error(client, ERROR_TOOMANY_ARGS, - "Too many arguments for help command"); + client->error(client, ERROR_TOOMANY_ARGS, + "Too many arguments for help command"); } if (subcommand == NULL) { @@ -381,8 +374,7 @@ server_run_command(struct client *client, struct config *config, if (memchr(argv[i]->iov_base, '\0', argv[i]->iov_len)) { notice("argument %lu from user %s contains nul octet", (unsigned long) i, user); - server_send_error(client, ERROR_BAD_COMMAND, - "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); goto done; } } @@ -398,14 +390,14 @@ server_run_command(struct client *client, struct config *config, notice("unknown command %s%s%s from user %s", command, (subcommand == NULL) ? "" : " ", (subcommand == NULL) ? "" : subcommand, user); - server_send_error(client, ERROR_UNKNOWN_COMMAND, "Unknown command"); + client->error(client, ERROR_UNKNOWN_COMMAND, "Unknown command"); goto done; } if (!server_config_acl_permit(rule, client)) { notice("access denied: user %s, command %s%s%s", user, command, (subcommand == NULL) ? "" : " ", (subcommand == NULL) ? "" : subcommand); - server_send_error(client, ERROR_ACCESS, "Access denied"); + client->error(client, ERROR_ACCESS, "Access denied"); goto done; } @@ -417,8 +409,8 @@ server_run_command(struct client *client, struct config *config, if (rule->help == NULL) { notice("command %s from user %s has no defined help", command, user); - server_send_error(client, ERROR_NO_HELP, - "No help defined for command"); + client->error(client, ERROR_NO_HELP, + "No help defined for command"); goto done; } else { free(subcommand); @@ -442,11 +434,9 @@ server_run_command(struct client *client, struct config *config, process.status = (signed int) WEXITSTATUS(process.status); else process.status = -1; - if (client->protocol == 1) - server_v1_send_output(client, process.output, process.status); - else - server_v2_send_status(client, process.status); + client->finish(client, process.output, process.status); } + status = process.status; done: free(command); @@ -461,6 +451,7 @@ server_run_command(struct client *client, struct config *config, evbuffer_free(process.input); if (process.output != NULL) evbuffer_free(process.output); + return status; } diff --git a/server/event-util.c b/server/event-util.c new file mode 100644 index 0000000..1c786b1 --- /dev/null +++ b/server/event-util.c @@ -0,0 +1,55 @@ +/* + * Utility functions for libevent. + * + * Provides some utility functions used with libevent in both remctld and + * remctl-shell. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2016 Dropbox, Inc. + * Copyright 2014 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/event.h> +#include <portable/socket.h> + +#include <server/internal.h> +#include <util/messages.h> + + +/* + * The logging callback for libevent. We hook this into our message system so + * that libevent messages are handled the same way as our other internal + * messages. This function should be passed to event_set_log_callback at the + * start of libevent initialization. + */ +void +server_event_log_callback(int severity, const char *message) +{ + switch (severity) { + case EVENT_LOG_DEBUG: + debug("%s", message); + break; + case EVENT_LOG_MSG: + notice("%s", message); + break; + default: + warn("%s", message); + break; + } +} + + +/* + * The fatal callback for libevent. Convert this to die, so that it's logged + * the same as our other messages. This function should be passed to + * event_set_fatal_callback at the start of libevent initialization. + */ +void +server_event_fatal_callback(int err) +{ + die("fatal libevent error (%d)", err); +} diff --git a/server/generic.c b/server/generic.c index 6980636..f26974d 100644 --- a/server/generic.c +++ b/server/generic.c @@ -1,11 +1,12 @@ /* - * Server implementation of generic protocol functions. + * Server implementation of generic GSS-API protocol functions. * - * These are the server protocol functions that can be shared between the v1 - * and v2 protocol. + * These are the server protocol functions that can use GSS-API but can be + * shared between the v1 and v2 protocol. * * Written by Russ Allbery <eagle@eyrie.org> * Based on work by Anton Ushakov + * Copyright 2016 Dropbox, Inc. * Copyright 2015 Russ Allbery <eagle@eyrie.org> * Copyright 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, * 2014 The Board of Trustees of the Leland Stanford Junior University @@ -155,6 +156,17 @@ server_new_client(int fd, gss_cred_id_t creds) } } + /* Based on the protocol, set up the callbacks. */ + if (client->protocol == 1) { + client->setup = server_v1_command_setup; + client->finish = server_v1_send_output; + client->error = server_v1_send_error; + } else { + client->setup = server_v2_command_setup; + client->finish = server_v2_command_finish; + client->error = server_v2_send_error; + } + /* Get the display version of the client name and store it. */ major = gss_display_name(&minor, name, &name_buf, &doid); if (major != GSS_S_COMPLETE) { @@ -206,8 +218,8 @@ server_free_client(struct client *client) /* - * Receives a command token payload and builds an argv structure for it, - * returning that as NULL-terminated array of pointers to struct iovecs. + * Parses a complete command token payload and builds an argv structure for + * it, returning that as NULL-terminated array of pointers to struct iovecs. * Takes the client struct, a pointer to the beginning of the payload * (starting with the argument count), and the length of the payload. If * there are any problems with the request, sends an error token, logs the @@ -228,17 +240,17 @@ server_parse_command(struct client *client, const char *buffer, size_t length) debug("argc is %lu", (unsigned long) argc); if (argc == 0) { warn("command with no arguments"); - server_send_error(client, ERROR_UNKNOWN_COMMAND, "Unknown command"); + client->error(client, ERROR_UNKNOWN_COMMAND, "Unknown command"); return NULL; } if (argc > COMMAND_MAX_ARGS) { warn("too large argc (%lu) in request message", (unsigned long) argc); - server_send_error(client, ERROR_TOOMANY_ARGS, "Too many arguments"); + client->error(client, ERROR_TOOMANY_ARGS, "Too many arguments"); return NULL; } if (length - (p - buffer) < 4 * argc) { warn("command data too short"); - server_send_error(client, ERROR_BAD_COMMAND, "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); return NULL; } argv = xcalloc(argc + 1, sizeof(struct iovec *)); @@ -252,8 +264,7 @@ server_parse_command(struct client *client, const char *buffer, size_t length) while (p <= buffer + length - 4) { if (count >= argc) { warn("sent more arguments than argc %lu", (unsigned long) argc); - server_send_error(client, ERROR_BAD_COMMAND, - "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); goto fail; } memcpy(&tmp, p, 4); @@ -261,8 +272,7 @@ server_parse_command(struct client *client, const char *buffer, size_t length) p += 4; if ((length - (p - buffer)) < arglen) { warn("command data invalid"); - server_send_error(client, ERROR_BAD_COMMAND, - "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); goto fail; } argv[count] = xmalloc(sizeof(struct iovec)); @@ -278,7 +288,7 @@ server_parse_command(struct client *client, const char *buffer, size_t length) } if (count != argc || p != buffer + length) { warn("argument count differs from arguments seen"); - server_send_error(client, ERROR_BAD_COMMAND, "Invalid command token"); + client->error(client, ERROR_BAD_COMMAND, "Invalid command token"); goto fail; } argv[count] = NULL; @@ -288,30 +298,3 @@ fail: server_free_command(argv); return NULL; } - - -/* - * Send an error back to the client. Takes the client struct, the error code, - * and the message to send and dispatches to the appropriate protocol-specific - * function. Returns true on success, false on failure. - */ -bool -server_send_error(struct client *client, enum error_codes error, - const char *message) -{ - struct evbuffer *buf; - bool result; - - if (client->protocol > 1) - return server_v2_send_error(client, error, message); - else { - buf = evbuffer_new(); - if (buf == NULL) - die("internal error: cannot create output buffer"); - if (evbuffer_add_printf(buf, "%s\n", message) < 0) - die("internal error: cannot add error message to buffer"); - result = server_v1_send_output(client, buf, -1); - evbuffer_free(buf); - return result; - } -} diff --git a/server/internal.h b/server/internal.h index f64a782..47e9857 100644 --- a/server/internal.h +++ b/server/internal.h @@ -2,6 +2,7 @@ * Internal support functions for the remctld daemon. * * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2016 Dropbox, Inc. * Copyright 2015 Russ Allbery <eagle@eyrie.org> * Copyright 2006, 2007, 2008, 2009, 2010, 2012, 2014 * The Board of Trustees of the Leland Stanford Junior University @@ -28,6 +29,7 @@ struct evbuffer; struct event; struct event_base; struct iovec; +struct process; /* * The maximum size of argc passed to the server (4K arguments), and the @@ -47,6 +49,7 @@ struct iovec; /* Holds the information about a client connection. */ struct client { int fd; /* File descriptor of client connection. */ + int stderr_fd; /* stderr file descriptor for remctl-shell. */ char *hostname; /* Hostname of client (if available). */ char *ipaddress; /* IP address of client as a string. */ int protocol; /* Protocol version number. */ @@ -57,6 +60,14 @@ struct client { time_t expires; /* Expiration time of GSS-API session. */ bool keepalive; /* Whether keep-alive was set. */ bool fatal; /* Whether a fatal error has occurred. */ + + /* + * Callbacks used by generic server code handle the separate protocols, + * set up when the client opens the connection. + */ + void (*setup)(struct process *); + bool (*finish)(struct client *, struct evbuffer *, int); + bool (*error)(struct client *, enum error_codes, const char *); }; /* Holds the configuration for a single command. */ @@ -136,30 +147,42 @@ bool server_config_acl_permit(const struct rule *, const struct client *); void server_config_set_gput_file(char *file); /* Running commands. */ -void server_run_command(struct client *, struct config *, struct iovec **); +int server_run_command(struct client *, struct config *, struct iovec **); /* Freeing the command structure. */ void server_free_command(struct iovec **); /* Running processes. */ bool server_process_run(struct process *process); +void server_handle_io_event(struct bufferevent *, short, void *); +void server_handle_input_end(struct bufferevent *, void *); -/* Generic protocol functions. */ +/* Generic GSS-API protocol functions. */ struct client *server_new_client(int fd, gss_cred_id_t creds); void server_free_client(struct client *); struct iovec **server_parse_command(struct client *, const char *, size_t); -bool server_send_error(struct client *, enum error_codes, const char *); /* Protocol v1 functions. */ +void server_v1_command_setup(struct process *); bool server_v1_send_output(struct client *, struct evbuffer *, int status); +bool server_v1_send_error(struct client *, enum error_codes, const char *); void server_v1_handle_messages(struct client *, struct config *); /* Protocol v2 functions. */ -bool server_v2_send_output(struct client *, int stream, struct evbuffer *); -bool server_v2_send_status(struct client *, int); +void server_v2_command_setup(struct process *); +bool server_v2_command_finish(struct client *, struct evbuffer *, int status); bool server_v2_send_error(struct client *, enum error_codes, const char *); void server_v2_handle_messages(struct client *, struct config *); +/* ssh protocol functions. */ +struct client *server_ssh_new_client(void); +void server_ssh_free_client(struct client *); +struct iovec **server_ssh_parse_command(const char *); + +/* libevent utility functions. */ +void server_event_log_callback(int, const char *); +void server_event_fatal_callback(int); + END_DECLS #endif /* !SERVER_INTERNAL_H */ diff --git a/server/process.c b/server/process.c index c1b68f5..d6b0dab 100644 --- a/server/process.c +++ b/server/process.c @@ -6,6 +6,7 @@ * with the child process. * * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2016 Dropbox, Inc. * Copyright 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, * 2014 The Board of Trustees of the Leland Stanford Junior University * @@ -48,13 +49,16 @@ /* - * Callback for events in input or output handling. This means either an - * error or EOF. On EOF or an EPIPE or ECONNRESET error, just deactivate the - * bufferevent. On other errors, send an error message to the client and then - * break out of the event loop. + * Callback for events in input or output handling while running a process. + * This means either an error or EOF. On EOF or an EPIPE or ECONNRESET error, + * just deactivate the bufferevent. On other errors, send an error message to + * the client and then break out of the event loop. + * + * This has to be public so that it can be referenced by the setup code for + * the various protocols. */ -static void -handle_io_event(struct bufferevent *bev, short events, void *data) +void +server_handle_io_event(struct bufferevent *bev, short events, void *data) { struct process *process = data; struct client *client = process->client; @@ -81,7 +85,7 @@ handle_io_event(struct bufferevent *bev, short events, void *data) syswarn("read from process failed"); else syswarn("write to standard input failed"); - server_send_error(client, ERROR_INTERNAL, "Internal failure"); + client->error(client, ERROR_INTERNAL, "Internal failure"); process->saw_error = true; event_base_loopbreak(process->loop); } @@ -90,10 +94,11 @@ handle_io_event(struct bufferevent *bev, short events, void *data) /* * Callback when all stdin data has been sent. We only have a callback to * shut down our end of the socketpair so that the process gets EOF on its - * next read. + * next read. Also has to be public so that it can be referenced in the + * per-protocol startup callbacks. */ -static void -handle_input_end(struct bufferevent *bev, void *data) +void +server_handle_input_end(struct bufferevent *bev, void *data) { struct process *process = data; @@ -104,84 +109,6 @@ handle_input_end(struct bufferevent *bev, void *data) /* - * Callback used to handle output from a process (protocol version two or - * later). We use the same handler for both standard output and standard - * error and check the bufferevent to determine which stream we're seeing. - * - * When called, note that we saw some output, which is a flag to continue - * processing when running the event loop after the child has exited. - */ -static void -handle_output(struct bufferevent *bev, void *data) -{ - int stream; - struct evbuffer *buf; - struct process *process = data; - - process->saw_output = true; - stream = (bev == process->inout) ? 1 : 2; - buf = bufferevent_get_input(bev); - if (!server_v2_send_output(process->client, stream, buf)) { - process->saw_error = true; - event_base_loopbreak(process->loop); - } -} - - -/* - * Discard all data in the evbuffer. This handler is used with protocol - * version one when we've already read as much data as we can return to the - * remctl client. - */ -static void -handle_output_discard(struct bufferevent *bev, void *data UNUSED) -{ - size_t length; - struct evbuffer *buf; - - buf = bufferevent_get_input(bev); - length = evbuffer_get_length(buf); - if (evbuffer_drain(buf, length) < 0) - sysdie("internal error: cannot discard extra output"); -} - - -/* - * Callback used to handle filling the output buffer with protocol version - * one. When this happens, we pull all of the data out into a separate - * evbuffer and then change our read callback to handle_output_discard, which - * just drains (discards) all subsequent data from the process. - */ -static void -handle_output_full(struct bufferevent *bev, void *data) -{ - struct process *process = data; - bufferevent_data_cb writecb; - - process->output = evbuffer_new(); - if (process->output == NULL) - die("internal error: cannot create discard evbuffer"); - if (bufferevent_read_buffer(bev, process->output) < 0) - die("internal error: cannot move data into output buffer"); - - /* - * Change the output callback. We need to be sure not to dump our input - * callback if it exists. - * - * After we see all the output that we can send to the client, we no - * longer care about error and EOF events, but if we set the callback to - * NULL here, we cause segfaults in libevent 1.4.x when we have both read - * and EOF events in the same event loop. So keep the error event handler - * since it doesn't hurt anything. This can safely be set to NULL once we - * require libevent 2.x. - */ - writecb = (process->input == NULL) ? NULL : handle_input_end; - bufferevent_setcb(bev, handle_output_discard, writecb, handle_io_event, - data); -} - - -/* * Called when the process has exited. Here we reap the status and then tell * the event loop to complete. Ignore SIGCHLD if our child process wasn't the * one that exited. @@ -222,7 +149,6 @@ start(evutil_socket_t junk UNUSED, short what UNUSED, void *data) struct process *process = data; struct client *client = process->client; struct event_base *loop = process->loop; - bufferevent_data_cb writecb = NULL; socket_type stdinout_fds[2] = { INVALID_SOCKET, INVALID_SOCKET }; socket_type stderr_fds[2] = { INVALID_SOCKET, INVALID_SOCKET }; socket_type fd; @@ -372,16 +298,18 @@ start(evutil_socket_t junk UNUSED, short what UNUSED, void *data) } /* - * Set up a bufferevent to consume output from the process. + * Set up bufferevents to send input to and consume output from the + * process. There are two possibilities here. * - * There are two possibilities here. For protocol version two, we use two - * bufferevents, one for standard input and output and one for standard - * error, that turn each chunk of data into a MESSAGE_OUTPUT token to the - * client. For protocol version one, we use a single bufferevent, which - * sends standard intput and collects both standard output and standard - * error, queuing it to send on process exit. In this case, stdinout_fd - * gets both streams, since there's no point in distinguishing, and we - * only need one bufferevent. + * For protocol version two, we use two bufferevents, one for standard + * input and output and one for standard error, that turn each chunk of + * data into a MESSAGE_OUTPUT token to the client. + * + * For protocol version one, we use a single bufferevent, which sends + * standard intput and collects both standard output and standard error, + * queuing it to send on process exit. In this case, stdinout_fd gets + * both streams, since there's no point in distinguishing, and we only + * need one bufferevent. */ fdflag_nonblocking(stdinout_fds[0], true); process->inout = bufferevent_socket_new(loop, process->stdinout_fd, 0); @@ -390,29 +318,19 @@ start(evutil_socket_t junk UNUSED, short what UNUSED, void *data) if (process->input == NULL) bufferevent_enable(process->inout, EV_READ); else { - writecb = handle_input_end; bufferevent_enable(process->inout, EV_READ | EV_WRITE); if (bufferevent_write_buffer(process->inout, process->input) < 0) die("internal error: cannot queue input for process"); } - if (client->protocol == 1) { - bufferevent_setcb(process->inout, handle_output_full, writecb, - handle_io_event, process); - bufferevent_setwatermark(process->inout, EV_READ, TOKEN_MAX_OUTPUT_V1, - TOKEN_MAX_OUTPUT_V1); - } else { - bufferevent_setcb(process->inout, handle_output, writecb, - handle_io_event, process); - bufferevent_setwatermark(process->inout, EV_READ, 0, TOKEN_MAX_OUTPUT); + if (client->protocol > 1) { fdflag_nonblocking(stderr_fds[0], true); process->err = bufferevent_socket_new(loop, process->stderr_fd, 0); if (process->err == NULL) die("internal error: cannot create stderr bufferevent"); - bufferevent_enable(process->err, EV_READ); - bufferevent_setcb(process->err, handle_output, NULL, - handle_io_event, process); - bufferevent_setwatermark(process->err, EV_READ, 0, TOKEN_MAX_OUTPUT); } + + /* Set up the event hooks for the different protocols. */ + client->setup(process); return; fail: @@ -424,7 +342,7 @@ fail: close(stderr_fds[0]); if (stderr_fds[1] != INVALID_SOCKET) close(stderr_fds[1]); - server_send_error(client, ERROR_INTERNAL, "Internal failure"); + client->error(client, ERROR_INTERNAL, "Internal failure"); process->saw_error = true; event_base_loopbreak(process->loop); } diff --git a/server/remctl-shell.c b/server/remctl-shell.c new file mode 100644 index 0000000..722432a --- /dev/null +++ b/server/remctl-shell.c @@ -0,0 +1,153 @@ +/* + * The remctl server as a restricted shell. + * + * This is a varient of remctld that, rather than listening to the network for + * the remctl protocol, runs as a restricted shell under ssh. It uses the + * same configuration and the same command semantics as the normal remctl + * server, but uses ssh as the transport mechanism and must be run under sshd. + * + * This file handles parsing of the user's command and the main control flow. + * + * Note that, because there's no good way to pass command-line options into a + * shell, it's not possible to reconfigure the configuration file path used by + * remctl-shell. + * + * Written by Russ Allbery + * Copyright 2016 Dropbox, Inc. + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/event.h> +#include <portable/system.h> + +#include <signal.h> +#include <syslog.h> + +#include <server/internal.h> +#include <util/messages.h> +#include <util/xmalloc.h> + +/* Usage message. */ +static const char usage_message[] = "\ +Usage: remctl-shell [-dhS] -c <command>\n\ +\n\ +Options:\n\ + -c <command> Specifies the command to run\n\ + -d Log verbose debugging information\n\ + -h Display this help\n\ + -S Log to standard output/error rather than syslog\n\ +\n\ +This is meant to be used as the shell for a dedicated account and handles\n\ +incoming commands via ssh. It must be run under ssh or with the same\n\ +environment variables ssh would set.\n"; + + +/* + * Display the usage message for remctl-shell. + */ +static void +usage(int status) +{ + FILE *output; + + output = (status == 0) ? stdout : stderr; + if (status != 0) + fprintf(output, "\n"); + fprintf(output, usage_message); + exit(status); +} + + +/* + * Main routine. Parses the configuration file and the user's command and + * then dispatches running the command. + */ +int +main(int argc, char *argv[]) +{ + int option, status; + bool debug = false; + bool log_stdout = false; + struct sigaction sa; + const char *command_string = NULL; + struct iovec **command; + struct client *client; + struct config *config; + + /* Ignore SIGPIPE errors from our children. */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + if (sigaction(SIGPIPE, &sa, NULL) < 0) + sysdie("cannot set SIGPIPE handler"); + + /* Establish identity for logging. */ + message_program_name = "remctl-shell"; + + /* Initialize the logging and fatal callbacks for libevent. */ + event_set_log_callback(server_event_log_callback); + event_set_fatal_callback(server_event_fatal_callback); + + /* + * Parse options. Since we're being run as a shell, there isn't all that + * much here. + */ + while ((option = getopt(argc, argv, "dc:hS")) != EOF) { + switch (option) { + case 'c': + command_string = optarg; + break; + case 'd': + debug = true; + break; + case 'h': + usage(0); + break; + case 'S': + log_stdout = true; + break; + default: + warn("unknown option -%c", optopt); + usage(1); + break; + } + } + if (command_string == NULL) + die("no command specified"); + + /* Set up logging. */ + if (log_stdout) { + if (debug) + message_handlers_debug(1, message_log_stdout); + } else { + openlog("remctl-shell", LOG_PID | LOG_NDELAY, LOG_DAEMON); + message_handlers_notice(1, message_log_syslog_info); + message_handlers_warn(1, message_log_syslog_warning); + message_handlers_die(1, message_log_syslog_err); + if (debug) + message_handlers_debug(1, message_log_syslog_debug); + } + + /* Read the configuration file. */ + config = server_config_load(CONFIG_FILE); + if (config == NULL) + die("cannot read configuration file %s", CONFIG_FILE); + + /* Create the client struct based on the ssh environment. */ + client = server_ssh_new_client(); + + /* Parse and execute the command. */ + command = server_ssh_parse_command(command_string); + if (command == NULL) + die("cannot parse command: %s", command_string); + status = server_run_command(client, config, command); + server_free_command(command); + + /* Clean up and exit. */ + server_ssh_free_client(client); + server_config_free(config); + libevent_global_shutdown(); + message_handlers_reset(); + return status; +} diff --git a/server/remctld.c b/server/remctld.c index 5fac8c7..bd561a8 100644 --- a/server/remctld.c +++ b/server/remctld.c @@ -152,41 +152,6 @@ exit_handler(int sig UNUSED) /* - * The logging callback for libevent. We hook this into our message system so - * that libevent messages are handled the same way as our other internal - * messages. This function should be passed to event_set_log_callback at the - * start of libevent initialization. - */ -static void -event_log_callback(int severity, const char *message) -{ - switch (severity) { - case EVENT_LOG_DEBUG: - debug("%s", message); - break; - case EVENT_LOG_MSG: - notice("%s", message); - break; - default: - warn("%s", message); - break; - } -} - - -/* - * The fatal callback for libevent. Convert this to die, so that it's logged - * the same as our other messages. This function should be passed to - * event_set_fatal_callback at the start of libevent initialization. - */ -static void -event_fatal_callback(int err) -{ - die("fatal libevent error (%d)", err); -} - - -/* * Given a service name, imports it and acquires credentials for it, storing * them in the second argument. Returns true on success and false on failure, * logging an error message. @@ -559,8 +524,8 @@ main(int argc, char *argv[]) message_program_name = "remctld"; /* Initialize the logging and fatal callbacks for libevent. */ - event_set_log_callback(event_log_callback); - event_set_fatal_callback(event_fatal_callback); + event_set_log_callback(server_event_log_callback); + event_set_fatal_callback(server_event_fatal_callback); /* Initialize options. */ memset(&options, 0, sizeof(options)); diff --git a/server/server-ssh.c b/server/server-ssh.c new file mode 100644 index 0000000..07bfb01 --- /dev/null +++ b/server/server-ssh.c @@ -0,0 +1,281 @@ +/* + * ssh protocol, server implementation. + * + * Implements remctl over ssh using regular commands and none of the remctl + * protocol. The only part of the normal remctl server reused here is the + * configuration and command running code. + * + * Written by Russ Allbery + * Copyright 2016 Dropbox, Inc. + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/event.h> +#include <portable/system.h> + +#include <ctype.h> + +#include <server/internal.h> +#include <util/buffer.h> +#include <util/macros.h> +#include <util/messages.h> +#include <util/vector.h> +#include <util/xmalloc.h> +#include <util/xwrite.h> + + +/* + * Parse a command string into a remctl command. This is much more complex + * for remctl-shell than it is for remctld since we get the command as a + * string with shell quoting and have to understand and undo the quoting. + * + * Implements single and double quotes, with backslash escaping any character. + */ +struct iovec ** +server_ssh_parse_command(const char *command) +{ + struct vector *args; + struct buffer *arg; + struct iovec **argv; + const char *p; + size_t i, length; + char quote = '\0'; + enum state { + SEPARATOR, + ARG, + QUOTE + } state; + + /* + * Parse the string using a state engine. We can be in one of three + * states: in the separator between arguments, or inside a quoted string. + * If inside a quoted string, the quote used to terminate the string is + * stored in quote. + * + * Backslash escapes any character inside or outside quotes. If backslash + * is at the end of the string, we just treat it as a literal backslash. + */ + args = vector_new(); + arg = buffer_new(); + state = SEPARATOR; + for (p = command; p != '\0'; p++) { + if (*p == '\\' && p[1] != '\0') { + buffer_append(arg, p + 1, 1); + if (state == SEPARATOR) + state = ARG; + continue; + } + switch (state) { + case SEPARATOR: + if (!isspace((int) *p)) { + switch (*p) { + case '\'': + case '"': + state = QUOTE; + quote = *p; + break; + default: + state = ARG; + buffer_append(arg, p, 1); + break; + } + } + break; + case QUOTE: + if (*p == quote) + state = ARG; + else + buffer_append(arg, p, 1); + break; + case ARG: + if (isspace((int) *p)) { + vector_addn(args, arg->data, arg->left); + buffer_set(arg, NULL, 0); + state = SEPARATOR; + } else { + switch (*p) { + case '\'': + case '"': + state = QUOTE; + quote = *p; + break; + default: + buffer_append(arg, p, 1); + break; + } + } + break; + } + } + + /* + * Ending inside a quoted string is an error. Otherwise, recover the last + * argument and clean up. + */ + if (state == QUOTE) { + warn("unterminated %c quote in command", quote); + goto fail; + } else if (state == ARG) { + vector_addn(args, arg->data, arg->left); + } + buffer_free(arg); + + /* Turn the vector into the iovec we need for everything else. */ + argv = xcalloc(args->count + 1, sizeof(struct iovec *)); + for (i = 0; i < args->count; i++) { + argv[i] = xcalloc(1, sizeof(struct iovec)); + length = strlen(args->strings[i]); + argv[i]->iov_base = xmalloc(length); + memcpy(argv[i]->iov_base, args->strings[i], length); + argv[i]->iov_len = length; + } + argv[args->count] = NULL; + vector_free(args); + return argv; + +fail: + vector_free(args); + buffer_free(arg); + return NULL; +} + + +/* + * Handle one block of output from the running command. + */ +static void +handle_output(struct bufferevent *bev, void *data) +{ + int fd; + struct evbuffer *buf; + struct process *process = data; + struct client *client = process->client; + + process->saw_output = true; + fd = (bev == process->inout) ? client->fd : client->stderr_fd; + buf = bufferevent_get_input(bev); + if (evbuffer_write(buf, fd) < 0) { + syswarn("error sending output"); + client->fatal = true; + process->saw_error = true; + event_base_loopbreak(process->loop); + } +} + + +/* + * Set up to execute a command. For the ssh protocol, all we need to do is + * install output handlers for both stdout and stderr that just send the + * output back to our stdout and stderr. + */ +static void +command_setup(struct process *process) +{ + bufferevent_data_cb writecb; + + writecb = (process->input == NULL) ? NULL : server_handle_input_end; + bufferevent_setcb(process->inout, handle_output, writecb, + server_handle_io_event, process); + bufferevent_setwatermark(process->inout, EV_READ, 0, TOKEN_MAX_OUTPUT); + bufferevent_enable(process->err, EV_READ); + bufferevent_setcb(process->err, handle_output, NULL, + server_handle_io_event, process); + bufferevent_setwatermark(process->err, EV_READ, 0, TOKEN_MAX_OUTPUT); +} + + +/* + * Handle the end of the command. For the ssh protocol, we do nothing, since + * the main program will collect the exit status and exit with the appropriate + * status in order to communicate it back to the caller. + */ +static bool +command_finish(struct client *client UNUSED, struct evbuffer *output UNUSED, + int exit_status UNUSED) +{ + return true; +} + + +/* + * Send an error back over an ssh channel. This just writes the error message + * with a trailing newline to standard error. + */ +static bool +send_error(struct client *client, enum error_codes code UNUSED, + const char *message) +{ + ssize_t status; + + status = xwrite(client->stderr_fd, message, strlen(message)); + if (status >= 0) + status = xwrite(client->stderr_fd, "\n", 1); + if (status < 0) { + syswarn("error sending error message"); + client->fatal = true; + return false; + } + return true; +} + + +/* + * Create a client struct for a remctl-shell invocation based on the ssh + * environment. Abort here if the expected ssh environment variables aren't + * set. Caller is responsible for freeing the allocated client struct. + */ +struct client * +server_ssh_new_client(void) +{ + struct client *client; + const char *ssh_client, *user; + struct vector *client_info; + + /* Parse client identity from ssh environment variables. */ + user = getenv("REMCTL_USER"); + if (user == NULL) + die("REMCTL_USER must be set in the environment via authorized_keys"); + ssh_client = getenv("SSH_CLIENT"); + if (ssh_client == NULL) + die("SSH_CLIENT not set (remctl-shell must be run via ssh)"); + client_info = vector_split_space(ssh_client, NULL); + + /* Create basic client struct. */ + client = xcalloc(1, sizeof(struct client)); + client->fd = STDOUT_FILENO; + client->stderr_fd = STDERR_FILENO; + client->ipaddress = xstrdup(client_info->strings[0]); + client->protocol = 3; + client->user = xstrdup(user); + + /* Add ssh protocol callbacks. */ + client->setup = command_setup; + client->finish = command_finish; + client->error = send_error; + + /* Free allocated data and return. */ + vector_free(client_info); + return client; +} + + +/* + * Free a client struct, including any resources that it holds. This is a + * subset of server_free_client that doesn't do the GSS-API actions. + */ +void +server_ssh_free_client(struct client *client) +{ + if (client == NULL) + return; + if (client->fd >= 0) + close(client->fd); + if (client->stderr_fd >= 0) + close(client->stderr_fd); + free(client->user); + free(client->hostname); + free(client->ipaddress); + free(client); +} diff --git a/server/server-v1.c b/server/server-v1.c index 0af9ab2..171b904 100644 --- a/server/server-v1.c +++ b/server/server-v1.c @@ -7,6 +7,7 @@ * * Written by Russ Allbery <eagle@eyrie.org> * Based on work by Anton Ushakov + * Copyright 2016 Dropbox, Inc. * Copyright 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2014 * The Board of Trustees of the Leland Stanford Junior University * @@ -22,11 +23,82 @@ #include <server/internal.h> #include <util/gss-tokens.h> +#include <util/macros.h> #include <util/messages.h> #include <util/xmalloc.h> /* + * Discard all data in the evbuffer. This handler is used with protocol + * version one when we've already read as much data as we can return to the + * remctl client. + */ +static void +handle_output_discard(struct bufferevent *bev, void *data UNUSED) +{ + size_t length; + struct evbuffer *buf; + + buf = bufferevent_get_input(bev); + length = evbuffer_get_length(buf); + if (evbuffer_drain(buf, length) < 0) + sysdie("internal error: cannot discard extra output"); +} + + +/* + * Callback used to handle filling the output buffer with protocol version + * one. When this happens, we pull all of the data out into a separate + * evbuffer and then change our read callback to handle_output_discard, which + * just drains (discards) all subsequent data from the process. + */ +static void +handle_output_full(struct bufferevent *bev, void *data) +{ + struct process *process = data; + bufferevent_data_cb writecb; + + process->output = evbuffer_new(); + if (process->output == NULL) + die("internal error: cannot create discard evbuffer"); + if (bufferevent_read_buffer(bev, process->output) < 0) + die("internal error: cannot move data into output buffer"); + + /* + * Change the output callback. We need to be sure not to dump our input + * callback if it exists. + * + * After we see all the output that we can send to the client, we no + * longer care about error and EOF events, but if we set the callback to + * NULL here, we cause segfaults in libevent 1.4.x when we have both read + * and EOF events in the same event loop. So keep the error event handler + * since it doesn't hurt anything. This can safely be set to NULL once we + * require libevent 2.x. + */ + writecb = (process->input == NULL) ? NULL : server_handle_input_end; + bufferevent_setcb(bev, handle_output_discard, writecb, + server_handle_io_event, data); +} + + +/* + * Set up handling of a child process with the v1 protocol. Takes the process + * struct sets up the necessary event loop hooks. + */ +void +server_v1_command_setup(struct process *process) +{ + bufferevent_data_cb writecb; + + writecb = (process->input == NULL) ? NULL : server_handle_input_end; + bufferevent_setcb(process->inout, handle_output_full, writecb, + server_handle_io_event, process); + bufferevent_setwatermark(process->inout, EV_READ, TOKEN_MAX_OUTPUT_V1, + TOKEN_MAX_OUTPUT_V1); +} + + +/* * Given the client struct, a buffer of data to send, and the exit status of a * command, send a protocol v1 output token back to the client. Returns true * on success and false on failure (and logs a message on failure). @@ -73,6 +145,29 @@ server_v1_send_output(struct client *client, struct evbuffer *output, /* + * Given the client struct, an error code, and an error message, send a + * protocol v1 error token to the client. Returns true on success, false on + * failure (and logs a message on failure). + */ +bool +server_v1_send_error(struct client *client, enum error_codes code UNUSED, + const char *message) +{ + struct evbuffer *buf; + bool result; + + buf = evbuffer_new(); + if (buf == NULL) + die("internal error: cannot create output buffer"); + if (evbuffer_add_printf(buf, "%s\n", message) < 0) + die("internal error: cannot add error message to buffer"); + result = server_v1_send_output(client, buf, -1); + evbuffer_free(buf); + return result; +} + + +/* * Takes the client struct and the server configuration and handles a client * request. Reads a command from the client, checks the ACL, runs the command * if appropriate, and sends any output back to the client. @@ -91,9 +186,9 @@ server_v1_handle_messages(struct client *client, struct config *config) if (status != TOKEN_OK) { warn_token("receiving command token", status, major, minor); if (status == TOKEN_FAIL_LARGE) - server_send_error(client, ERROR_TOOMUCH_DATA, "Too much data"); + client->error(client, ERROR_TOOMUCH_DATA, "Too much data"); else if (status != TOKEN_FAIL_EOF) - server_send_error(client, ERROR_BAD_TOKEN, "Invalid token"); + client->error(client, ERROR_BAD_TOKEN, "Invalid token"); return; } @@ -101,7 +196,7 @@ server_v1_handle_messages(struct client *client, struct config *config) if (token.length > TOKEN_MAX_DATA) { warn("command data length %lu exceeds 64KB", (unsigned long) token.length); - server_send_error(client, ERROR_TOOMUCH_DATA, "Too much data"); + client->error(client, ERROR_TOOMUCH_DATA, "Too much data"); gss_release_buffer(&minor, &token); return; } diff --git a/server/server-v2.c b/server/server-v2.c index c609815..c208d23 100644 --- a/server/server-v2.c +++ b/server/server-v2.c @@ -5,6 +5,7 @@ * * Written by Russ Allbery <eagle@eyrie.org> * Based on work by Anton Ushakov + * Copyright 2016 Dropbox, Inc. * Copyright 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2014 * The Board of Trustees of the Leland Stanford Junior University * @@ -20,6 +21,7 @@ #include <server/internal.h> #include <util/gss-tokens.h> +#include <util/macros.h> #include <util/messages.h> #include <util/xmalloc.h> @@ -30,7 +32,7 @@ * buffer in the client struct. Returns true on success, false on failure * (and logs a message on failure). */ -bool +static bool server_v2_send_output(struct client *client, int stream, struct evbuffer *output) { @@ -80,12 +82,59 @@ server_v2_send_output(struct client *client, int stream, /* + * Callback used to handle output from a process (protocol version two or + * later). We use the same handler for both standard output and standard + * error and check the bufferevent to determine which stream we're seeing. + * + * When called, note that we saw some output, which is a flag to continue + * processing when running the event loop after the child has exited. + */ +static void +handle_output(struct bufferevent *bev, void *data) +{ + int stream; + struct evbuffer *buf; + struct process *process = data; + + process->saw_output = true; + stream = (bev == process->inout) ? 1 : 2; + buf = bufferevent_get_input(bev); + if (!server_v2_send_output(process->client, stream, buf)) { + process->saw_error = true; + event_base_loopbreak(process->loop); + } +} + + +/* + * Set up handling of a child process with the v2 protocol. Takes the process + * struct and sets up the necessary event loop hooks. + */ +void +server_v2_command_setup(struct process *process) +{ + bufferevent_data_cb writecb; + + writecb = (process->input == NULL) ? NULL : server_handle_input_end; + bufferevent_setcb(process->inout, handle_output, writecb, + server_handle_io_event, process); + bufferevent_setwatermark(process->inout, EV_READ, 0, TOKEN_MAX_OUTPUT); + bufferevent_enable(process->err, EV_READ); + bufferevent_setcb(process->err, handle_output, NULL, + server_handle_io_event, process); + bufferevent_setwatermark(process->err, EV_READ, 0, TOKEN_MAX_OUTPUT); +} + + +/* * Given the client struct and the exit status, send a protocol v2 status * token to the client. Returns true on success, false on failure (and logs a - * message on failure). + * message on failure). Takes an ignored buffer argument for call + * compatibility with protocol v1. */ bool -server_v2_send_status(struct client *client, int exit_status) +server_v2_command_finish(struct client *client, struct evbuffer *output UNUSED, + int exit_status) { gss_buffer_desc token; char buffer[1 + 1 + 1]; @@ -241,7 +290,7 @@ server_v2_read_token(struct client *client, gss_buffer_t token) if (status != TOKEN_OK) { warn_token("receiving token", status, major, minor); if (status != TOKEN_FAIL_EOF && status != TOKEN_FAIL_SOCKET) - server_send_error(client, ERROR_BAD_TOKEN, "Invalid token"); + client->error(client, ERROR_BAD_TOKEN, "Invalid token"); } return status; } @@ -278,8 +327,7 @@ server_v2_read_continuation(struct client *client, gss_buffer_t token) return false; } else if (p[1] != MESSAGE_COMMAND) { warn("unexpected message type %d from client", (int) p[1]); - server_send_error(client, ERROR_UNEXPECTED_MESSAGE, - "Unexpected message"); + client->error(client, ERROR_UNEXPECTED_MESSAGE, "Unexpected message"); return false; } return true; @@ -320,16 +368,16 @@ server_v2_handle_command(struct client *client, struct config *config, if (token->length > TOKEN_MAX_DATA) { warn("command data length %lu exceeds 64KB", (unsigned long) token->length); - result = server_send_error(client, ERROR_TOOMUCH_DATA, - "Too much data"); + result = client->error(client, ERROR_TOOMUCH_DATA, + "Too much data"); goto fail; } /* Make sure the continuation is sane. */ if ((p[3] == 1 && continued) || (p[3] > 1 && !continued) || p[3] > 3) { warn("bad continue status %d", (int) p[3]); - result = server_send_error(client, ERROR_BAD_COMMAND, - "Invalid command token"); + result = client->error(client, ERROR_BAD_COMMAND, + "Invalid command token"); goto fail; } continued = (p[3] == 1 || p[3] == 2); @@ -344,8 +392,8 @@ server_v2_handle_command(struct client *client, struct config *config, if (length >= COMMAND_MAX_DATA - total) { warn("total command length %lu exceeds %lu", length + total, COMMAND_MAX_DATA); - result = server_send_error(client, ERROR_TOOMUCH_DATA, - "Too much data"); + result = client->error(client, ERROR_TOOMUCH_DATA, + "Too much data"); goto fail; } if (continued || buffer != NULL) { @@ -426,8 +474,8 @@ server_v2_handle_token(struct client *client, struct config *config, break; default: warn("unknown message type %d from client", (int) p[1]); - result = server_send_error(client, ERROR_UNKNOWN_MESSAGE, - "Unknown message"); + result = client->error(client, ERROR_UNKNOWN_MESSAGE, + "Unknown message"); break; } return result; |