summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile.am50
-rw-r--r--server/commands.c59
-rw-r--r--server/event-util.c55
-rw-r--r--server/generic.c63
-rw-r--r--server/internal.h33
-rw-r--r--server/process.c144
-rw-r--r--server/remctl-shell.c153
-rw-r--r--server/remctld.c39
-rw-r--r--server/server-ssh.c281
-rw-r--r--server/server-v1.c101
-rw-r--r--server/server-v2.c76
-rw-r--r--tests/TESTS1
-rw-r--r--tests/server/acl-t.c7
-rw-r--r--tests/server/acl/localgroup-t.c4
-rw-r--r--tests/util/buffer-t.c289
-rw-r--r--util/buffer.c320
-rw-r--r--util/buffer.h158
18 files changed, 1567 insertions, 268 deletions
diff --git a/.gitignore b/.gitignore
index 0cec309..d402c2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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 */