diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile.am | 50 | ||||
-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 | ||||
-rw-r--r-- | tests/TESTS | 1 | ||||
-rw-r--r-- | tests/server/acl-t.c | 7 | ||||
-rw-r--r-- | tests/server/acl/localgroup-t.c | 4 | ||||
-rw-r--r-- | tests/util/buffer-t.c | 289 | ||||
-rw-r--r-- | util/buffer.c | 320 | ||||
-rw-r--r-- | util/buffer.h | 158 |
18 files changed, 1567 insertions, 268 deletions
@@ -56,6 +56,7 @@ /ruby/test_remctl.rb /remctl-*.tar.gz /remctl-*.tar.xz +/server/remctl-shell /server/remctld /stamp-h1 /stamp-python @@ -109,6 +110,7 @@ /tests/server/summary-t /tests/server/user-t /tests/server/version-t +/tests/util/buffer-t /tests/util/fdflag-t /tests/util/gss-tokens-t /tests/util/messages-krb5-t diff --git a/Makefile.am b/Makefile.am index 431e5a8..ea5a759 100644 --- a/Makefile.am +++ b/Makefile.am @@ -134,12 +134,12 @@ portable_libportable_la_SOURCES = portable/dummy.c portable/event.h \ portable/sd-daemon.h portable/socket.h portable/stdbool.h \ portable/system.h portable/uio.h portable_libportable_la_LIBADD = $(LTLIBOBJS) -util_libutil_la_SOURCES = util/fdflag.c util/fdflag.h util/gss-errors.c \ - util/gss-errors.h util/gss-tokens.c util/gss-tokens.h util/macros.h \ - util/messages.c util/messages.h util/network.c util/network.h \ - util/protocol.h util/tokens.c util/tokens.h util/vector.c \ - util/vector.h util/xmalloc.c util/xmalloc.h util/xwrite.c \ - util/xwrite.h +util_libutil_la_SOURCES = util/buffer.c util/buffer.h util/fdflag.c \ + util/fdflag.h util/gss-errors.c util/gss-errors.h util/gss-tokens.c \ + util/gss-tokens.h util/macros.h util/messages.c util/messages.h \ + util/network.c util/network.h util/protocol.h util/tokens.c \ + util/tokens.h util/vector.c util/vector.h util/xmalloc.c \ + util/xmalloc.h util/xwrite.c util/xwrite.h util_libutil_la_LDFLAGS = $(GSSAPI_LDFLAGS) util_libutil_la_LIBADD = $(GSSAPI_LIBS) @@ -182,16 +182,16 @@ client_remctl_SOURCES = client/remctl.c client_remctl_LDADD = client/libremctl.la util/libutil.la \ portable/libportable.la -# The remctld server. We include portable/event-extra.c directly in the -# sources for the server instead of including it in libportable to avoid -# introducing a libevent dependency in libremctl, since apparently the -# linker isn't smart enough to figure out that the event functions are -# hidden and never called and optimize them out. -sbin_PROGRAMS = server/remctld -server_remctld_SOURCES = portable/event-extra.c server/commands.c \ - server/config.c server/generic.c server/logging.c server/internal.h \ - server/process.c server/remctld.c server/server-v1.c \ - server/server-v2.c +# The remctld and remctl-shell servers. We include portable/event-extra.c +# directly in the sources for the server instead of including it in +# libportable to avoid introducing a libevent dependency in libremctl, since +# apparently the linker isn't smart enough to figure out that the event +# functions are hidden and never called and optimize them out. +sbin_PROGRAMS = server/remctld server/remctl-shell +server_remctld_SOURCES = portable/event-extra.c server/commands.c \ + server/config.c server/event-util.c server/generic.c \ + server/logging.c server/internal.h server/process.c \ + server/remctld.c server/server-v1.c server/server-v2.c server_remctld_CPPFLAGS = -DCONFIG_FILE=\"$(sysconfdir)/remctl.conf\" \ $(GSSAPI_CPPFLAGS) $(KRB5_CPPFLAGS) $(GPUT_CPPFLAGS) \ $(PCRE_CPPFLAGS) $(LIBEVENT_CPPFLAGS) $(SYSTEMD_CFLAGS) @@ -200,6 +200,16 @@ server_remctld_LDFLAGS = $(GSSAPI_LDFLAGS) $(KRB5_LDFLAGS) $(GPUT_LDFLAGS) \ server_remctld_LDADD = util/libutil.la portable/libportable.la \ $(GSSAPI_LIBS) $(KRB5_LIBS) $(GPUT_LIBS) $(PCRE_LIBS) \ $(LIBEVENT_LIBS) $(SYSTEMD_LIBS) +server_remctl_shell_SOURCES = portable/event-extra.c server/commands.c \ + server/config.c server/event-util.c server/logging.c \ + server/internal.h server/process.c server/remctl-shell.c \ + server/server-ssh.c +server_remctl_shell_CPPFLAGS = -DCONFIG_FILE=\"$(sysconfdir)/remctl.conf\" \ + $(GPUT_CPPFLAGS) $(PCRE_CPPFLAGS) $(LIBEVENT_CPPFLAGS) +server_remctl_shell_LDFLAGS = $(GPUT_LDFLAGS) $(PCRE_LDFLAGS) \ + $(LIBEVENT_LDFLAGS) +server_remctl_shell_LDADD = util/libutil.la portable/libportable.la \ + $(GPUT_LIBS) $(PCRE_LIBS) $(LIBEVENT_LIBS) # Install the systemd unit file if systemd support was detected. if HAVE_SYSTEMD @@ -349,7 +359,7 @@ check_PROGRAMS = tests/runtests tests/client/api-t tests/client/ccache-t \ tests/server/help-t tests/server/invalid-t tests/server/logging-t \ tests/server/noop-t tests/server/stdin-t tests/server/streaming-t \ tests/server/summary-t tests/server/user-t tests/server/version-t \ - tests/util/fdflag-t tests/util/gss-tokens-t \ + tests/util/buffer-t tests/util/fdflag-t tests/util/gss-tokens-t \ tests/util/messages-krb5-t tests/util/messages-t \ tests/util/network/addr-ipv4-t tests/util/network/addr-ipv6-t \ tests/util/network/client-t tests/util/network/server-t \ @@ -370,8 +380,8 @@ tests_tap_libtap_a_SOURCES = tests/tap/basic.c tests/tap/basic.h \ # Used for server tests. SERVER_FILES = portable/event-extra.c server/commands.c server/config.c \ - server/generic.c server/logging.c server/process.c \ - server/server-v1.c server/server-v2.c + server/event-util.c server/generic.c server/logging.c \ + server/process.c server/server-v1.c server/server-v2.c # All of the test programs. tests_client_api_t_LDFLAGS = $(KRB5_LDFLAGS) @@ -508,6 +518,8 @@ tests_server_version_t_LDFLAGS = $(GSSAPI_LDFLAGS) $(KRB5_LDFLAGS) \ tests_server_version_t_LDADD = client/libremctl.la tests/tap/libtap.a \ util/libutil.la portable/libportable.la $(GSSAPI_LIBS) $(KRB5_LIBS) \ $(PCRE_LIBS) +tests_util_buffer_t_LDADD = tests/tap/libtap.a util/libutil.la \ + portable/libportable.la tests_util_fdflag_t_LDADD = tests/tap/libtap.a util/libutil.la \ portable/libportable.la tests_util_gss_tokens_t_SOURCES = tests/util/faketoken.c \ 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; diff --git a/tests/TESTS b/tests/TESTS index 062e5de..499145c 100644 --- a/tests/TESTS +++ b/tests/TESTS @@ -38,6 +38,7 @@ server/streaming server/summary server/user server/version +util/buffer util/gss-tokens util/messages util/messages-krb5 diff --git a/tests/server/acl-t.c b/tests/server/acl-t.c index c448d78..060503a 100644 --- a/tests/server/acl-t.c +++ b/tests/server/acl-t.c @@ -2,6 +2,7 @@ * Test suite for the server ACL checking. * * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2016 Dropbox, Inc. * Copyright 2015, 2016 Russ Allbery <eagle@eyrie.org> * Copyright 2007, 2008, 2009, 2010, 2012, 2014 * The Board of Trustees of the Leland Stanford Junior University @@ -35,7 +36,8 @@ static bool acl_permit(const struct rule *rule, const char *user) { struct client client = { - -1, NULL, NULL, 0, NULL, (char *) user, false, 0, 0, false, false + -1, -1, NULL, NULL, 0, NULL, (char *) user, false, 0, 0, false, false, + NULL, NULL, NULL }; return server_config_acl_permit(rule, &client); } @@ -54,7 +56,8 @@ acl_permit_anonymous(const struct rule *rule) { static char *pname = NULL; struct client client = { - -1, NULL, NULL, 0, NULL, NULL, true, 0, 0, false, false + -1, -1, NULL, NULL, 0, NULL, NULL, true, 0, 0, false, false, NULL, + NULL, NULL }; if (pname == NULL) diff --git a/tests/server/acl/localgroup-t.c b/tests/server/acl/localgroup-t.c index b2bf166..7632acc 100644 --- a/tests/server/acl/localgroup-t.c +++ b/tests/server/acl/localgroup-t.c @@ -5,6 +5,7 @@ * * Written by Remi Ferrand <remi.ferrand@cc.in2p3.fr> * Revisions by Russ Allbery <eagle@eyrie.org> + * Copyright 2016 Dropbox, Inc. * Copyright 2015 Russ Allbery <eagle@eyrie.org> * Copyright 2014 IN2P3 Computing Centre - CNRS * Copyright 2014 @@ -67,7 +68,8 @@ static bool acl_permit(const struct rule *rule, const char *user) { struct client client = { - -1, NULL, NULL, 0, NULL, (char *) user, false, 0, 0, false, false + -1, -1, NULL, NULL, 0, NULL, (char *) user, false, 0, 0, false, false, + NULL, NULL, NULL }; return server_config_acl_permit(rule, &client); } diff --git a/tests/util/buffer-t.c b/tests/util/buffer-t.c new file mode 100644 index 0000000..ad070ac --- /dev/null +++ b/tests/util/buffer-t.c @@ -0,0 +1,289 @@ +/* + * buffer test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <http://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * + * The authors hereby relinquish any claim to any copyright that they may have + * in this work, whether granted under contract or by operation of law or + * international treaty, and hereby commit to the public, at large, that they + * shall not, at any time in the future, seek to enforce any copyright in this + * work against any person or entity, or prevent any person or entity from + * copying, publishing, distributing or creating derivative works of this + * work. + */ + +#include <config.h> +#include <portable/system.h> + +#include <fcntl.h> + +#include <tests/tap/basic.h> +#include <util/buffer.h> +#include <util/xwrite.h> + +static const char test_string1[] = "This is a test"; +static const char test_string2[] = " of the buffer system"; +static const char test_string3[] = "This is a test\0 of the buffer system"; + + +/* + * Test buffer_vsprintf. Wrapper needed to generate the va_list. + */ +static void __attribute__((__format__(printf, 2, 3))) +test_vsprintf(struct buffer *buffer, const char *format, ...) +{ + va_list args; + + va_start(args, format); + buffer_vsprintf(buffer, format, args); + va_end(args); +} + + +/* + * Likewise for buffer_append_vsprintf. + */ +static void __attribute__((__format__(printf, 2, 3))) +test_append_vsprintf(struct buffer *buffer, const char *format, ...) +{ + va_list args; + + va_start(args, format); + buffer_append_vsprintf(buffer, format, args); + va_end(args); +} + + +int +main(void) +{ + struct buffer one = { 0, 0, 0, NULL }; + struct buffer two = { 0, 0, 0, NULL }; + struct buffer *three; + int fd; + char *data; + ssize_t count; + size_t offset; + + plan(89); + + /* buffer_set, buffer_append, buffer_swap */ + buffer_set(&one, test_string1, sizeof(test_string1)); + is_int(1024, one.size, "minimum size is 1024"); + is_int(0, one.used, "used starts at 0"); + is_int(sizeof(test_string1), one.left, "left is correct"); + is_string(test_string1, one.data, "data is corect"); + buffer_append(&one, test_string2, sizeof(test_string2)); + is_int(1024, one.size, "appended data doesn't change size"); + is_int(0, one.used, "or used"); + is_int(sizeof(test_string3), one.left, "but left is the right size"); + ok(memcmp(one.data, test_string3, sizeof(test_string3)) == 0, + "and the resulting data is correct"); + one.left -= sizeof(test_string1); + one.used += sizeof(test_string1); + buffer_append(&one, test_string1, sizeof(test_string1)); + is_int(1024, one.size, "size still isn't larger after adding data"); + is_int(sizeof(test_string1), one.used, "and used is preserved on append"); + is_int(sizeof(test_string3), one.left, "and left is updated properly"); + ok(memcmp(one.data + one.used, test_string2, sizeof(test_string2)) == 0, + "and the middle data is unchanged"); + ok(memcmp(one.data + one.used + sizeof(test_string2), test_string1, + sizeof(test_string1)) == 0, "and the final data is correct"); + buffer_set(&one, test_string1, sizeof(test_string1)); + buffer_set(&two, test_string2, sizeof(test_string2)); + buffer_swap(&one, &two); + is_int(1024, one.size, "swap #1 size is correct"); + is_int(0, one.used, "swap #1 used is correct"); + is_int(sizeof(test_string2), one.left, "swap #1 left is correct"); + is_string(test_string2, one.data, "swap #1 data is correct"); + is_int(1024, two.size, "swap #2 size is correct"); + is_int(0, two.used, "swap #2 used is correct"); + is_int(sizeof(test_string1), two.left, "swap #2 left is correct"); + is_string(test_string1, two.data, "swap #2 data is correct"); + free(one.data); + free(two.data); + one.data = NULL; + two.data = NULL; + one.size = 0; + two.size = 0; + + /* buffer_resize */ + three = buffer_new(); + ok(three != NULL, "buffer_new works"); + if (three == NULL) + bail("buffer_new returned NULL"); + is_int(0, three->size, "initial size is 0"); + buffer_set(three, test_string1, sizeof(test_string1)); + is_int(1024, three->size, "size becomes 1024 when adding data"); + buffer_resize(three, 512); + is_int(1024, three->size, "resizing to something smaller doesn't change"); + buffer_resize(three, 1025); + is_int(2048, three->size, "resizing to something larger goes to 2048"); + buffer_free(three); + + /* buffer_read, buffer_find_string, buffer_compact */ + fd = open("buffer-test", O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + sysbail("cannot create buffer-test"); + data = bmalloc(2048); + memset(data, 'a', 1023); + data[1023] = '\r'; + data[1024] = '\n'; + memset(data + 1025, 'b', 1023); + if (xwrite(fd, data, 2048) < 2048) + sysbail("cannot write to buffer-test"); + if (lseek(fd, 0, SEEK_SET) == (off_t) -1) + sysbail("cannot rewind buffer-test"); + three = buffer_new(); + ok(three != NULL, "buffer_new works"); + if (three == NULL) + bail("buffer_new returned NULL"); + is_int(0, three->size, "and initial size is 0"); + buffer_resize(three, 1024); + is_int(1024, three->size, "resize to 1024 works"); + count = buffer_read(three, fd); + is_int(1024, count, "reading into a buffer of size 1024 reads 1024"); + offset = 0; + ok(!buffer_find_string(three, "\r\n", 0, &offset), + "buffer_find_string with truncated string fails"); + is_int(0, offset, "and offset is unchanged"); + ok(memcmp(three->data, data, three->size) == 0, "buffer data is correct"); + buffer_resize(three, 2048); + is_int(2048, three->size, "resizing the buffer to 2048 works"); + count = buffer_read(three, fd); + is_int(1024, count, "and now we can read the rest of the data"); + ok(memcmp(three->data, data, 2048) == 0, "and it's all there"); + ok(!buffer_find_string(three, "\r\n", 1024, &offset), + "buffer_find_string with a string starting before offset fails"); + is_int(0, offset, "and offset is unchanged"); + ok(buffer_find_string(three, "\r\n", 0, &offset), + "finding the string on the whole buffer works"); + is_int(1023, offset, "and returns the correct location"); + three->used += 400; + three->left -= 400; + buffer_compact(three); + is_int(2048, three->size, "compacting buffer doesn't change the size"); + is_int(0, three->used, "but used is now zero"); + is_int(1648, three->left, "and left is decreased appropriately"); + ok(memcmp(three->data, data + 400, 1648) == 0, "and the data is correct"); + count = buffer_read(three, fd); + is_int(0, count, "reading at EOF returns 0"); + close(fd); + unlink("buffer-test"); + free(data); + buffer_free(three); + + /* buffer_sprintf and buffer_append_sprintf */ + three = buffer_new(); + buffer_append_sprintf(three, "testing %d testing", 6); + is_int(0, three->used, "buffer_append_sprintf doesn't change used"); + is_int(17, three->left, "but sets left correctly"); + buffer_append(three, "", 1); + is_int(18, three->left, "appending a nul works"); + is_string("testing 6 testing", three->data, "and the data is correct"); + three->left--; + three->used += 5; + three->left -= 5; + buffer_append_sprintf(three, " %d", 7); + is_int(14, three->left, "appending a digit works"); + buffer_append(three, "", 1); + is_string("testing 6 testing 7", three->data, "and the data is correct"); + buffer_sprintf(three, "%d testing", 8); + is_int(9, three->left, "replacing the buffer works"); + is_string("8 testing", three->data, "and the results are correct"); + data = bmalloc(1050); + memset(data, 'a', 1049); + data[1049] = '\0'; + is_int(1024, three->size, "size before large sprintf is 1024"); + buffer_sprintf(three, "%s", data); + is_int(2048, three->size, "size after large sprintf is 2048"); + is_int(1049, three->left, "and left is correct"); + buffer_append(three, "", 1); + is_string(data, three->data, "and data is correct"); + free(data); + buffer_free(three); + + /* buffer_read_all */ + fd = open("buffer-test", O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + sysbail("cannot create buffer-test"); + data = bmalloc(2049); + memset(data, 'a', 2049); + if (xwrite(fd, data, 2049) < 2049) + sysbail("cannot write to buffer-test"); + if (lseek(fd, 0, SEEK_SET) == (off_t) -1) + sysbail("cannot rewind buffer-test"); + three = buffer_new(); + ok(buffer_read_all(three, fd), "buffer_read_all succeeds"); + is_int(0, three->used, "and unused is zero"); + is_int(2049, three->left, "and left is correct"); + is_int(4096, three->size, "and size is correct"); + ok(memcmp(data, three->data, 2049) == 0, "and data is correct"); + if (lseek(fd, 0, SEEK_SET) == (off_t) -1) + sysbail("cannot rewind buffer-test"); + ok(buffer_read_all(three, fd), "reading again succeeds"); + is_int(0, three->used, "and used is correct"); + is_int(4098, three->left, "and left is now larger"); + is_int(8192, three->size, "and size doubled"); + ok(memcmp(data, three->data + 2049, 2049) == 0, "and data is correct"); + + /* buffer_read_file */ + if (lseek(fd, 0, SEEK_SET) == (off_t) -1) + sysbail("cannot rewind buffer-test"); + buffer_free(three); + three = buffer_new(); + ok(buffer_read_file(three, fd), "buffer_read_file succeeds"); + is_int(0, three->used, "and leaves unused at 0"); + is_int(2049, three->left, "and left is correct"); + is_int(3072, three->size, "and size is a multiple of 1024"); + ok(memcmp(data, three->data, 2049) == 0, "and the data is correct"); + + /* buffer_read_all and buffer_read_file errors */ + close(fd); + ok(!buffer_read_all(three, fd), "buffer_read_all on closed fd fails"); + is_int(3072, three->size, "and size is unchanged"); + ok(!buffer_read_file(three, fd), "buffer_read_file on closed fd fails"); + is_int(3072, three->size, "and size is unchanged"); + is_int(2049, three->left, "and left is unchanged"); + unlink("buffer-test"); + free(data); + buffer_free(three); + + /* buffer_vsprintf and buffer_append_vsprintf */ + three = buffer_new(); + test_append_vsprintf(three, "testing %d testing", 6); + is_int(0, three->used, "buffer_append_vsprintf leaves used as 0"); + is_int(17, three->left, "and left is correct"); + buffer_append(three, "", 1); + is_int(18, three->left, "and left is correct after appending a nul"); + is_string("testing 6 testing", three->data, "and data is correct"); + three->left--; + three->used += 5; + three->left -= 5; + test_append_vsprintf(three, " %d", 7); + is_int(14, three->left, "and appending results in the correct left"); + buffer_append(three, "", 1); + is_string("testing 6 testing 7", three->data, "and the right data"); + test_vsprintf(three, "%d testing", 8); + is_int(9, three->left, "replacing the buffer results in the correct size"); + is_string("8 testing", three->data, "and the correct data"); + data = bmalloc(1050); + memset(data, 'a', 1049); + data[1049] = '\0'; + is_int(1024, three->size, "size is 1024 before large vsprintf"); + test_vsprintf(three, "%s", data); + is_int(2048, three->size, "and 2048 afterwards"); + is_int(1049, three->left, "and left is correct"); + buffer_append(three, "", 1); + is_string(data, three->data, "and data is correct"); + free(data); + buffer_free(three); + + /* Test buffer_free with NULL and ensure it doesn't explode. */ + buffer_free(NULL); + + return 0; +} diff --git a/util/buffer.c b/util/buffer.c new file mode 100644 index 0000000..7ad4767 --- /dev/null +++ b/util/buffer.c @@ -0,0 +1,320 @@ +/* + * Counted, reusable memory buffer. + * + * A buffer is an allocated block of memory with a known size and a separate + * data length. It's intended to store strings and can be reused repeatedly + * to minimize the number of memory allocations. Buffers increase in + * increments of 1K, or double for some operations. + * + * A buffer contains a record of what data has been used and what data is as + * yet unprocessed, used when the buffer is an I/O buffer where lots of data + * is buffered and then slowly processed out of the buffer. The total length + * of the data is used + left. If a buffer is just used to store some data, + * used can be set to 0 and left stores the length of the data. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <http://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2011, 2012, 2014 + * The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2004, 2005, 2006 + * by Internet Systems Consortium, Inc. ("ISC") + * Copyright (c) 1991, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, + * 2002, 2003 by The Internet Software Consortium and Rich Salz + * + * This code is derived from software contributed to the Internet Software + * Consortium by Rich Salz. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include <config.h> +#include <portable/system.h> + +#include <errno.h> +#include <sys/stat.h> + +#include <util/buffer.h> +#include <util/xmalloc.h> + + +/* + * Allocate a new struct buffer and initialize it. + */ +struct buffer * +buffer_new(void) +{ + return xcalloc(1, sizeof(struct buffer)); +} + + +/* + * Free a buffer. + */ +void +buffer_free(struct buffer *buffer) +{ + if (buffer == NULL) + return; + free(buffer->data); + free(buffer); +} + + +/* + * Resize a buffer to be at least as large as the provided second argument. + * Resize buffers to multiples of 1KB to keep the number of reallocations to a + * minimum. Refuse to resize a buffer to make it smaller. + */ +void +buffer_resize(struct buffer *buffer, size_t size) +{ + if (size < buffer->size) + return; + buffer->size = (size + 1023) & ~1023UL; + buffer->data = xrealloc(buffer->data, buffer->size); +} + + +/* + * Compact a buffer by moving the data between buffer->used and buffer->left + * to the beginning of the buffer, overwriting the already-consumed data. + */ +void +buffer_compact(struct buffer *buffer) +{ + if (buffer->used == 0) + return; + if (buffer->left != 0) + memmove(buffer->data, buffer->data + buffer->used, buffer->left); + buffer->used = 0; +} + + +/* + * Replace whatever data is currently in the buffer with the provided data. + * Resize the buffer if needed. + */ +void +buffer_set(struct buffer *buffer, const char *data, size_t length) +{ + if (length > 0) { + buffer_resize(buffer, length); + memmove(buffer->data, data, length); + } + buffer->left = length; + buffer->used = 0; +} + + +/* + * Append data to a buffer. The new data shows up as additional unused data + * at the end of the buffer. Resize the buffer if needed. + */ +void +buffer_append(struct buffer *buffer, const char *data, size_t length) +{ + size_t total; + + if (length == 0) + return; + total = buffer->used + buffer->left; + buffer_resize(buffer, total + length); + buffer->left += length; + memcpy(buffer->data + total, data, length); +} + + +/* + * Print data into a buffer from the supplied va_list, appending to the end. + * The new data shows up as unused data at the end of the buffer. The + * trailing nul is not added to the buffer. + */ +void +buffer_append_vsprintf(struct buffer *buffer, const char *format, va_list args) +{ + size_t total, avail; + ssize_t status; + va_list args_copy; + + total = buffer->used + buffer->left; + avail = buffer->size - total; + va_copy(args_copy, args); + status = vsnprintf(buffer->data + total, avail, format, args_copy); + va_end(args_copy); + if (status < 0) + return; + if ((size_t) status + 1 <= avail) { + buffer->left += status; + } else { + buffer_resize(buffer, total + status + 1); + avail = buffer->size - total; + status = vsnprintf(buffer->data + total, avail, format, args); + if (status < 0 || (size_t) status + 1 > avail) + return; + buffer->left += status; + } +} + + +/* + * Print data into a buffer, appending to the end. The new data shows up as + * unused data at the end of the buffer. Resize the buffer if needed. The + * trailing nul is not added to the buffer. + */ +void +buffer_append_sprintf(struct buffer *buffer, const char *format, ...) +{ + va_list args; + + va_start(args, format); + buffer_append_vsprintf(buffer, format, args); + va_end(args); +} + + +/* + * Replace the current buffer contents with data printed from the supplied + * va_list. The new data shows up as unused data at the end of the buffer. + * The trailing nul is not added to the buffer. + */ +void +buffer_vsprintf(struct buffer *buffer, const char *format, va_list args) +{ + buffer_set(buffer, NULL, 0); + buffer_append_vsprintf(buffer, format, args); +} + + +/* + * Replace the current buffer contents with data printed from the supplied + * format string and arguments. The new data shows up as unused data at the + * end of the buffer. The trailing nul is not added to the buffer. + */ +void +buffer_sprintf(struct buffer *buffer, const char *format, ...) +{ + va_list args; + + va_start(args, format); + buffer_vsprintf(buffer, format, args); + va_end(args); +} + + +/* + * Swap the contents of two buffers. + */ +void +buffer_swap(struct buffer *one, struct buffer *two) +{ + struct buffer tmp; + + tmp = *one; + *one = *two; + *two = tmp; +} + + +/* + * Find a given string in the unconsumed data in buffer. We know that all the + * data prior to start (an offset into the space between buffer->used and + * buffer->left) has already been searched. Returns the offset of the string + * (with the same meaning as start) in offset if found, and returns true if + * the terminator is found and false otherwise. + */ +bool +buffer_find_string(struct buffer *buffer, const char *string, size_t start, + size_t *offset) +{ + char *terminator, *data; + size_t length; + + if (buffer->data == NULL) + return false; + length = strlen(string); + do { + data = buffer->data + buffer->used + start; + terminator = memchr(data, string[0], buffer->left - start); + if (terminator == NULL) + return false; + start = (terminator - buffer->data) - buffer->used; + if (buffer->left - start < length) + return false; + start++; + } while (memcmp(terminator, string, length) != 0); + *offset = start - 1; + return true; +} + + +/* + * Read from a file descriptor into a buffer, up to the available space in the + * buffer, and return the number of characters read. + */ +ssize_t +buffer_read(struct buffer *buffer, int fd) +{ + ssize_t count; + + do { + size_t used = buffer->used + buffer->left; + count = read(fd, buffer->data + used, buffer->size - used); + } while (count == -1 && (errno == EAGAIN || errno == EINTR)); + if (count > 0) + buffer->left += count; + return count; +} + + +/* + * Read from a file descriptor until end of file is reached, doubling the + * buffer size as necessary to hold all of the data. Returns true on success, + * false on failure (in which case errno will be set). + */ +bool +buffer_read_all(struct buffer *buffer, int fd) +{ + ssize_t count; + + if (buffer->size == 0) + buffer_resize(buffer, 1024); + do { + size_t used = buffer->used + buffer->left; + if (buffer->size <= used) + buffer_resize(buffer, buffer->size * 2); + count = buffer_read(buffer, fd); + } while (count > 0); + return (count == 0); +} + + +/* + * Read the entire contents of a file into a buffer. This is a slight + * optimization over buffer_read_all because it can stat the file descriptor + * first and size the buffer appropriately. buffer_read_all will still handle + * the case where the file size changes while it's being read. Returns true + * on success, false on failure (in which case errno will be set). + */ +bool +buffer_read_file(struct buffer *buffer, int fd) +{ + struct stat st; + size_t used = buffer->used + buffer->left; + + if (fstat(fd, &st) < 0) + return false; + buffer_resize(buffer, st.st_size + used); + return buffer_read_all(buffer, fd); +} diff --git a/util/buffer.h b/util/buffer.h new file mode 100644 index 0000000..754d42e --- /dev/null +++ b/util/buffer.h @@ -0,0 +1,158 @@ +/* + * Counted, reusable memory buffer. + * + * A buffer is an allocated block of memory with a known size and a separate + * data length. It's intended to store strings and can be reused repeatedly + * to minimize the number of memory allocations. Buffers increase in + * increments of 1K, or double for some operations. + * + * A buffer contains a record of what data has been used and what data is as + * yet unprocessed, used when the buffer is an I/O buffer where lots of data + * is buffered and then slowly processed out of the buffer. The total length + * of the data is used + left. If a buffer is just used to store some data, + * used can be set to 0 and left stores the length of the data. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <http://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2014 Russ Allbery <eagle@eyrie.org> + * Copyright 2011, 2012 + * The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2004, 2005, 2006 + * by Internet Systems Consortium, Inc. ("ISC") + * Copyright (c) 1991, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, + * 2002, 2003 by The Internet Software Consortium and Rich Salz + * + * This code is derived from software contributed to the Internet Software + * Consortium by Rich Salz. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef UTIL_BUFFER_H +#define UTIL_BUFFER_H 1 + +#include <config.h> +#include <portable/macros.h> +#include <portable/stdbool.h> + +#include <stdarg.h> +#include <sys/types.h> + +struct buffer { + size_t size; /* Total allocated length. */ + size_t used; /* Data already used. */ + size_t left; /* Remaining unused data. */ + char *data; /* Pointer to allocated memory. */ +}; + +BEGIN_DECLS + +/* Default to a hidden visibility for all util functions. */ +#pragma GCC visibility push(hidden) + +/* Allocate a new buffer and initialize its contents. */ +struct buffer *buffer_new(void) + __attribute__((__warn_unused_result__, __malloc__)); + +/* Free an allocated buffer. */ +void buffer_free(struct buffer *); + +/* + * Resize a buffer to be at least as large as the provided size. Invalidates + * pointers into the buffer. + */ +void buffer_resize(struct buffer *, size_t) + __attribute__((__nonnull__)); + +/* + * Compact a buffer, removing all used data and moving unused data to the + * beginning of the buffer. Invalidates pointers into the buffer. + */ +void buffer_compact(struct buffer *) + __attribute__((__nonnull__)); + +/* + * Set the buffer contents, ignoring anything currently there. If length is + * 0, empties the buffer, in which case data may be NULL. + */ +void buffer_set(struct buffer *, const char *data, size_t length) + __attribute__((__nonnull__(1))); + +/* + * Set the buffer contents via a sprintf-style format string. No trailing + * nul is added. + */ +void buffer_sprintf(struct buffer *, const char *, ...) + __attribute__((__format__(printf, 2, 3), __nonnull__)); +void buffer_vsprintf(struct buffer *, const char *, va_list) + __attribute__((__format__(printf, 2, 0), __nonnull__)); + +/* Append data to the buffer. */ +void buffer_append(struct buffer *, const char *data, size_t length) + __attribute__((__nonnull__(1))); + +/* Append via an sprintf-style format string. No trailing nul is added. */ +void buffer_append_sprintf(struct buffer *, const char *, ...) + __attribute__((__format__(printf, 2, 3), __nonnull__)); +void buffer_append_vsprintf(struct buffer *, const char *, va_list) + __attribute__((__format__(printf, 2, 0), __nonnull__)); + +/* Swap the contents of two buffers. */ +void buffer_swap(struct buffer *, struct buffer *) + __attribute__((__nonnull__)); + +/* + * Find the given string in the unconsumed data in a buffer. start is an + * offset into the unused data specifying where to start the search (to save + * time with multiple searches). Pass 0 to start the search at the beginning + * of the unused data. Returns true if the terminator is found, putting the + * offset (into the unused data space) of the beginning of the terminator into + * the fourth argument. Returns false if the terminator isn't found. + */ +bool buffer_find_string(struct buffer *, const char *, size_t start, + size_t *offset) + __attribute__((__nonnull__)); + +/* + * Read from a file descriptor into a buffer, up to the available space in the + * buffer. Return the number of characters read. Retries the read if + * interrupted by a signal or if it returns EAGAIN, but stops on any other + * error or after any successful read. Returns -1 on an error reading from + * the file descriptor and sets errno. + */ +ssize_t buffer_read(struct buffer *, int fd) + __attribute__((__nonnull__)); + +/* + * Read from a file descriptor into a buffer until end of file is reached. + * Returns true on success and false (setting errno) on error. + */ +bool buffer_read_all(struct buffer *, int fd) + __attribute__((__nonnull__)); + +/* + * Read the contents of a file into a buffer. This should be used instead of + * buffer_read_all when fstat can be called on the file descriptor. Returns + * true on success and false (setting errno) on error. + */ +bool buffer_read_file(struct buffer *, int fd) + __attribute__((__nonnull__)); + +/* Undo default visibility change. */ +#pragma GCC visibility pop + +END_DECLS + +#endif /* UTIL_BUFFER_H */ |