/* * Copyright (c) 2019 Todd C. Miller * * 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 THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR 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 #include #include #include #include #include #include #include #include #ifdef HAVE_STDBOOL_H # include #else # include "compat/stdbool.h" #endif /* HAVE_STDBOOL_H */ #if defined(HAVE_STDINT_H) # include #elif defined(HAVE_INTTYPES_H) # include #endif #include #include #include #include #include #include "log_server.pb-c.h" #include "sudo_gettext.h" /* must be included before sudo_compat.h */ #include "sudo_compat.h" #include "sudo_conf.h" #include "sudo_debug.h" #include "sudo_util.h" #include "sudo_event.h" #include "sudo_fatal.h" #include "sudo_iolog.h" #include "sendlog.h" #ifndef HAVE_GETADDRINFO # include "compat/getaddrinfo.h" #endif #ifdef HAVE_GETOPT_LONG # include # else # include "compat/getopt.h" #endif /* HAVE_GETOPT_LONG */ static struct iolog_file iolog_files[IOFD_MAX]; static char *iolog_dir; static void usage(bool fatal) { fprintf(stderr, "usage: %s [-h host] [-i iolog-id] [-p port] " "[-r restart-point] /path/to/iolog\n", getprogname()); exit(1); } static void help(void) { (void)printf(_("%s - send sudo I/O log to remote server\n\n"), getprogname()); usage(false); (void)puts(_("\nOptions:\n" " --help display help message and exit\n" " -h, --host host to send logs to\n" " -i, --iolog_id remote ID of I/O log to be resumed\n" " -p, --port port to use when connecting to host\n" " -r, --restart restart previous I/O log transfer\n" " -V, --version display version information and exit\n")); exit(0); } /* * Connect to specified host:port * If host has multiple addresses, the first one that connects is used. * Returns open socket or -1 on error. */ static int connect_server(const char *host, const char *port) { struct addrinfo hints, *res, *res0; const char *cause = "getaddrinfo"; int error, sock, save_errno; debug_decl(connect_server, SUDO_DEBUG_UTIL) memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; error = getaddrinfo(host, port, &hints, &res0); if (error != 0) { sudo_warnx(U_("unable to look up %s:%s: %s"), host, port, gai_strerror(error)); debug_return_int(-1); } sock = -1; for (res = res0; res; res = res->ai_next) { sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sock == -1) { cause = "socket"; continue; } if (connect(sock, res->ai_addr, res->ai_addrlen) == -1) { cause = "connect"; save_errno = errno; close(sock); errno = save_errno; sock = -1; continue; } break; /* success */ } if (sock != -1) { int flags = fcntl(sock, F_GETFL, 0); if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) { cause = "fcntl(O_NONBLOCK)"; save_errno = errno; close(sock); errno = save_errno; sock = -1; } } if (sock == -1) sudo_warn("%s", cause); freeaddrinfo(res0); debug_return_int(sock); } /* * Free client closure contents. */ static void client_closure_free(struct client_closure *closure) { debug_decl(client_closure_free, SUDO_DEBUG_UTIL) if (closure != NULL) { sudo_ev_free(closure->read_ev); sudo_ev_free(closure->write_ev); free(closure->read_buf.data); free(closure->write_buf.data); free(closure->buf); } debug_return; } /* * Read the next I/O buffer as described by closure->timing. */ static bool read_io_buf(struct client_closure *closure) { struct timing_closure *timing = &closure->timing; const char *errstr = NULL; size_t nread; debug_decl(read_io_buf, SUDO_DEBUG_UTIL) if (!iolog_files[timing->event].enabled) { errno = ENOENT; sudo_warn("%s/%s", iolog_dir, iolog_fd_to_name(timing->event)); debug_return_bool(false); } /* Expand buf as needed. */ if (timing->u.nbytes > closure->bufsize) { free(closure->buf); closure->bufsize = bufsize_roundup(timing->u.nbytes); if ((closure->buf = malloc(closure->bufsize)) == NULL) { sudo_warn(NULL); timing->u.nbytes = 0; debug_return_bool(false); } } nread = iolog_read(&iolog_files[timing->event], closure->buf, timing->u.nbytes, &errstr); if (nread != timing->u.nbytes) { sudo_warnx(U_("unable to read %s/%s: %s"), iolog_dir, iolog_fd_to_name(timing->event), errstr); debug_return_bool(false); } debug_return_bool(true); } /* * Format a ClientMessage and store the wire format message in buf. * Returns true on success, false on failure. */ static bool fmt_client_message(struct connection_buffer *buf, ClientMessage *msg) { uint32_t msg_len; bool ret = false; size_t len; debug_decl(fmt_client_message, SUDO_DEBUG_UTIL) len = client_message__get_packed_size(msg); if (len > MESSAGE_SIZE_MAX) { sudo_warnx(U_("client message too large: %zu\n"), len); goto done; } /* Wire message size is used for length encoding, precedes message. */ msg_len = htonl((uint32_t)len); len += sizeof(msg_len); /* Resize buffer as needed. */ if (len > buf->size) { free(buf->data); buf->size = bufsize_roundup(len); if ((buf->data = malloc(buf->size)) == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to malloc %u", buf->size); buf->size = 0; goto done; } } memcpy(buf->data, &msg_len, sizeof(msg_len)); client_message__pack(msg, buf->data + sizeof(msg_len)); buf->len = len; ret = true; done: debug_return_bool(ret); } /* * Split command + args into an array of strings. * Returns an array containing command and args, reusing space in "command". * Note that the returned array does not end with a terminating NULL. */ static char ** split_command(char *command, size_t *lenp) { char *cp; char **args; size_t len; debug_decl(split_command, SUDO_DEBUG_UTIL) for (cp = command, len = 0;;) { len++; if ((cp = strchr(cp, ' ')) == NULL) break; cp++; } args = reallocarray(NULL, len, sizeof(char *)); if (args == NULL) debug_return_ptr(NULL); for (cp = command, len = 0;;) { args[len++] = cp; if ((cp = strchr(cp, ' ')) == NULL) break; *cp++ = '\0'; } *lenp = len; debug_return_ptr(args); } /* * Build and format an AcceptMessage wrapped in a ClientMessage. * Stores the wire format message in the closure's write buffer. * Returns true on success, false on failure. */ static bool fmt_accept_message(struct client_closure *closure) { ClientMessage client_msg = CLIENT_MESSAGE__INIT; AcceptMessage accept_msg = ACCEPT_MESSAGE__INIT; TimeSpec tv = TIME_SPEC__INIT; InfoMessage__StringList runargv = INFO_MESSAGE__STRING_LIST__INIT; struct iolog_info *log_info = closure->log_info; char *hostname; bool ret = false; size_t n; debug_decl(fmt_accept_message, SUDO_DEBUG_UTIL) /* * Fill in AcceptMessage and add it to ClientMessage. */ if ((hostname = sudo_gethostname()) == NULL) { sudo_warn("gethostname"); debug_return_bool(false); } /* Sudo I/O logs only store start time in seconds. */ tv.tv_sec = log_info->tstamp; tv.tv_nsec = 0; accept_msg.submit_time = &tv; /* Client will send IoBuffer messages. */ accept_msg.expect_iobufs = true; /* Split command into a StringList. */ runargv.strings = split_command(log_info->cmd, &runargv.n_strings); if (runargv.strings == NULL) sudo_fatal(NULL); /* The sudo I/O log info file has limited info. */ accept_msg.n_info_msgs = 10; accept_msg.info_msgs = calloc(accept_msg.n_info_msgs, sizeof(InfoMessage *)); if (accept_msg.info_msgs == NULL) debug_return_bool(false); for (n = 0; n < accept_msg.n_info_msgs; n++) { accept_msg.info_msgs[n] = malloc(sizeof(InfoMessage)); if (accept_msg.info_msgs[n] == NULL) { accept_msg.n_info_msgs = n; goto done; } info_message__init(accept_msg.info_msgs[n]); } /* Fill in info_msgs */ n = 0; accept_msg.info_msgs[n]->key = "command"; accept_msg.info_msgs[n]->strval = log_info->cmd; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; accept_msg.info_msgs[n]->key = "columns"; accept_msg.info_msgs[n]->numval = log_info->cols; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; n++; accept_msg.info_msgs[n]->key = "cwd"; accept_msg.info_msgs[n]->strval = log_info->cwd; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; accept_msg.info_msgs[n]->key = "lines"; accept_msg.info_msgs[n]->numval = log_info->lines; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; n++; accept_msg.info_msgs[n]->key = "runargv"; accept_msg.info_msgs[n]->strlistval = &runargv; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRLISTVAL; n++; if (log_info->runas_group != NULL) { accept_msg.info_msgs[n]->key = "rungroup"; accept_msg.info_msgs[n]->strval = log_info->runas_group; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; } accept_msg.info_msgs[n]->key = "runuser"; accept_msg.info_msgs[n]->strval = log_info->runas_user; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; accept_msg.info_msgs[n]->key = "submithost"; accept_msg.info_msgs[n]->strval = hostname; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; accept_msg.info_msgs[n]->key = "submituser"; accept_msg.info_msgs[n]->strval = log_info->user; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; accept_msg.info_msgs[n]->key = "ttyname"; accept_msg.info_msgs[n]->strval = log_info->tty; accept_msg.info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; n++; /* Update n_info_msgs. */ accept_msg.n_info_msgs = n; sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending AcceptMessage, array length %zu", __func__, n); /* Schedule ClientMessage */ client_msg.accept_msg = &accept_msg; client_msg.type_case = CLIENT_MESSAGE__TYPE_ACCEPT_MSG; ret = fmt_client_message(&closure->write_buf, &client_msg); if (ret) { if (sudo_ev_add(NULL, closure->write_ev, NULL, false) == -1) ret = false; } done: for (n = 0; n < accept_msg.n_info_msgs; n++) { free(accept_msg.info_msgs[n]); } free(accept_msg.info_msgs); free(hostname); debug_return_bool(ret); } /* * Build and format a RestartMessage wrapped in a ClientMessage. * Stores the wire format message in the closure's write buffer. * Returns true on success, false on failure. */ static bool fmt_restart_message(struct client_closure *closure) { ClientMessage client_msg = CLIENT_MESSAGE__INIT; RestartMessage restart_msg = RESTART_MESSAGE__INIT; TimeSpec tv = TIME_SPEC__INIT; bool ret = false; debug_decl(fmt_restart_message, SUDO_DEBUG_UTIL) sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending RestartMessage, [%lld, %ld]", __func__, (long long)closure->restart->tv_sec, closure->restart->tv_nsec); tv.tv_sec = closure->restart->tv_sec; tv.tv_nsec = closure->restart->tv_nsec; restart_msg.resume_point = &tv; restart_msg.log_id = (char *)closure->iolog_id; /* Schedule ClientMessage */ client_msg.restart_msg = &restart_msg; client_msg.type_case = CLIENT_MESSAGE__TYPE_RESTART_MSG; ret = fmt_client_message(&closure->write_buf, &client_msg); if (ret) { if (sudo_ev_add(NULL, closure->write_ev, NULL, false) == -1) ret = false; } ret = true; debug_return_bool(ret); } /* * Build and format an ExitMessage wrapped in a ClientMessage. * Stores the wire format message in the closure's write buffer. * Returns true on success, false on failure. */ static bool fmt_exit_message(struct client_closure *closure) { ClientMessage client_msg = CLIENT_MESSAGE__INIT; ExitMessage exit_msg = EXIT_MESSAGE__INIT; bool ret = false; debug_decl(fmt_exit_message, SUDO_DEBUG_UTIL) /* * We don't have enough data in a sudo I/O log to create a real * exit message. For example, the exit value and run time are * not known. This results in a zero-sized message. */ exit_msg.exit_value = 0; sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ExitMessage, exit value %d", __func__, exit_msg.exit_value); /* Send ClientMessage */ client_msg.exit_msg = &exit_msg; client_msg.type_case = CLIENT_MESSAGE__TYPE_EXIT_MSG; if (!fmt_client_message(&closure->write_buf, &client_msg)) goto done; ret = true; done: debug_return_bool(ret); } /* * Build and format an IoBuffer wrapped in a ClientMessage. * Stores the wire format message in buf. * Returns true on success, false on failure. */ static bool fmt_io_buf(int type, struct client_closure *closure, struct connection_buffer *buf) { ClientMessage client_msg = CLIENT_MESSAGE__INIT; IoBuffer iobuf_msg = IO_BUFFER__INIT; TimeSpec delay = TIME_SPEC__INIT; bool ret = false; debug_decl(fmt_io_buf, SUDO_DEBUG_UTIL) if (!read_io_buf(closure)) goto done; /* Fill in IoBuffer. */ /* TODO: split buffer if it is too large */ delay.tv_sec = closure->timing.delay.tv_sec; delay.tv_nsec = closure->timing.delay.tv_nsec; iobuf_msg.delay = &delay; iobuf_msg.data.data = (void *)closure->buf; iobuf_msg.data.len = closure->timing.u.nbytes; sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending IoBuffer length %zu, type %d, size %zu", __func__, iobuf_msg.data.len, type, io_buffer__get_packed_size(&iobuf_msg)); /* Send ClientMessage, it doesn't matter which IoBuffer we set. */ client_msg.ttyout_buf = &iobuf_msg; client_msg.type_case = type; if (!fmt_client_message(buf, &client_msg)) goto done; ret = true; done: debug_return_bool(ret); } /* * Build and format a ChangeWindowSize message wrapped in a ClientMessage. * Stores the wire format message in buf. * Returns true on success, false on failure. */ static bool fmt_winsize(struct client_closure *closure, struct connection_buffer *buf) { ClientMessage client_msg = CLIENT_MESSAGE__INIT; ChangeWindowSize winsize_msg = CHANGE_WINDOW_SIZE__INIT; TimeSpec delay = TIME_SPEC__INIT; struct timing_closure *timing = &closure->timing; bool ret = false; debug_decl(fmt_winsize, SUDO_DEBUG_UTIL) /* Fill in ChangeWindowSize message. */ delay.tv_sec = timing->delay.tv_sec; delay.tv_nsec = timing->delay.tv_nsec; winsize_msg.delay = &delay; winsize_msg.rows = timing->u.winsize.lines; winsize_msg.cols = timing->u.winsize.cols; sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ChangeWindowSize, %dx%d", __func__, winsize_msg.rows, winsize_msg.cols); /* Send ClientMessage */ client_msg.winsize_event = &winsize_msg; client_msg.type_case = CLIENT_MESSAGE__TYPE_WINSIZE_EVENT; if (!fmt_client_message(buf, &client_msg)) goto done; ret = true; done: debug_return_bool(ret); } /* * Build and format a CommandSuspend message wrapped in a ClientMessage. * Stores the wire format message in buf. * Returns true on success, false on failure. */ static bool fmt_suspend(struct client_closure *closure, struct connection_buffer *buf) { ClientMessage client_msg = CLIENT_MESSAGE__INIT; CommandSuspend suspend_msg = COMMAND_SUSPEND__INIT; TimeSpec delay = TIME_SPEC__INIT; struct timing_closure *timing = &closure->timing; bool ret = false; debug_decl(fmt_suspend, SUDO_DEBUG_UTIL) /* Fill in CommandSuspend message. */ delay.tv_sec = timing->delay.tv_sec; delay.tv_nsec = timing->delay.tv_nsec; suspend_msg.delay = &delay; if (sig2str(timing->u.signo, closure->buf) == -1) goto done; suspend_msg.signal = closure->buf; sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending CommandSuspend, SIG%s", __func__, suspend_msg.signal); /* Send ClientMessage */ client_msg.suspend_event = &suspend_msg; client_msg.type_case = CLIENT_MESSAGE__TYPE_SUSPEND_EVENT; if (!fmt_client_message(buf, &client_msg)) goto done; ret = true; done: debug_return_bool(ret); } /* * Read the next entry for the I/O log timing file and format a ClientMessage. * Stores the wire format message in the closure's write buffer. * Returns true on success, false on failure. */ static bool fmt_next_iolog(struct client_closure *closure) { struct timing_closure *timing = &closure->timing; struct connection_buffer *buf = &closure->write_buf; bool ret = false; debug_decl(fmt_next_iolog, SUDO_DEBUG_UTIL) if (buf->len != 0) { sudo_warnx(U_("%s: write buffer already in use"), __func__); debug_return_bool(false); } /* TODO: fill write buffer with multiple messages */ again: switch (read_timing_record(&iolog_files[IOFD_TIMING], timing)) { case 0: /* OK */ break; case 1: /* no more IO buffers */ closure->state = SEND_EXIT; debug_return_bool(fmt_exit_message(closure)); case -1: default: debug_return_bool(false); } /* Track elapsed time for comparison with commit points. */ sudo_timespecadd(&timing->delay, closure->elapsed, closure->elapsed); /* If we have a restart point, ignore records until we hit it. */ if (closure->restart != NULL) { if (sudo_timespeccmp(closure->restart, closure->elapsed, >=)) goto again; closure->restart = NULL; /* caught up */ } switch (timing->event) { case IO_EVENT_STDIN: ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDIN_BUF, closure, buf); break; case IO_EVENT_STDOUT: ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDOUT_BUF, closure, buf); break; case IO_EVENT_STDERR: ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDERR_BUF, closure, buf); break; case IO_EVENT_TTYIN: ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYIN_BUF, closure, buf); break; case IO_EVENT_TTYOUT: ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYOUT_BUF, closure, buf); break; case IO_EVENT_WINSIZE: ret = fmt_winsize(closure, buf); break; case IO_EVENT_SUSPEND: ret = fmt_suspend(closure, buf); break; default: sudo_warnx(U_("unexpected I/O event %d"), timing->event); break; } debug_return_bool(ret); } /* * Additional work to do after a ClientMessage was sent to the server. * Advances state and formats the next ClientMessage (if any). */ static bool client_message_completion(struct client_closure *closure) { debug_decl(client_message_completion, SUDO_DEBUG_UTIL) switch (closure->state) { case SEND_ACCEPT: case SEND_RESTART: closure->state = SEND_IO; /* FALLTHROUGH */ case SEND_IO: /* fmt_next_iolog() will advance state on EOF. */ if (!fmt_next_iolog(closure)) debug_return_bool(false); break; case SEND_EXIT: /* Done writing, just waiting for final commit point. */ sudo_ev_del(NULL, closure->write_ev); closure->state = CLOSING; break; default: sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); debug_return_bool(false); } debug_return_bool(true); } /* * Respond to a ServerHello message from the server. * Returns true on success, false on error. */ static bool handle_server_hello(ServerHello *msg, struct client_closure *closure) { size_t n; debug_decl(handle_server_hello, SUDO_DEBUG_UTIL) if (closure->state != RECV_HELLO) { sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); debug_return_bool(false); } /* Sanity check ServerHello message. */ if (msg->server_id == NULL || msg->server_id[0] == '\0') { sudo_warnx("%s", U_("invalid ServerHello")); debug_return_bool(false); } printf("server ID: %s\n", msg->server_id); /* TODO: handle redirect */ if (msg->redirect != NULL && msg->redirect[0] != '\0') printf("redirect: %s\n", msg->redirect); for (n = 0; n < msg->n_servers; n++) { printf("server %zu: %s\n", n + 1, msg->servers[n]); } debug_return_bool(true); } /* * Respond to a CommitPoint message from the server. * Returns true on success, false on error. */ static bool handle_commit_point(TimeSpec *commit_point, struct client_closure *closure) { debug_decl(handle_commit_point, SUDO_DEBUG_UTIL) /* Only valid after we have sent an IO buffer. */ if (closure->state < SEND_IO) { sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: commit point: [%lld, %d]", __func__, (long long)commit_point->tv_sec, commit_point->tv_nsec); closure->committed.tv_sec = commit_point->tv_sec; closure->committed.tv_nsec = commit_point->tv_nsec; debug_return_bool(true); } /* * Respond to a LogId message from the server. * Always returns true. */ static bool handle_log_id(char *id, struct client_closure *closure) { debug_decl(handle_log_id, SUDO_DEBUG_UTIL) printf("remote log ID: %s\n", id); if ((closure->iolog_id = strdup(id)) == NULL) sudo_fatal(NULL); debug_return_bool(true); } /* * Respond to a ServerError message from the server. * Always returns false. */ static bool handle_server_error(char *errmsg, struct client_closure *closure) { debug_decl(handle_server_error, SUDO_DEBUG_UTIL) sudo_warnx(U_("error message received from server: %s"), errmsg); debug_return_bool(false); } /* * Respond to a ServerAbort message from the server. * Always returns false. */ static bool handle_server_abort(char *errmsg, struct client_closure *closure) { debug_decl(handle_server_abort, SUDO_DEBUG_UTIL) sudo_warnx(U_("abort message received from server: %s"), errmsg); debug_return_bool(false); } /* * Respond to a ServerMessage from the server. * Returns true on success, false on error. */ static bool handle_server_message(uint8_t *buf, size_t len, struct client_closure *closure) { ServerMessage *msg; bool ret = false; debug_decl(handle_server_message, SUDO_DEBUG_UTIL) sudo_debug_printf(SUDO_DEBUG_INFO, "%s: unpacking ServerMessage", __func__); msg = server_message__unpack(NULL, len, buf); if (msg == NULL) { sudo_warnx("%s", U_("unable to unpack ServerMessage")); debug_return_bool(false); } switch (msg->type_case) { case SERVER_MESSAGE__TYPE_HELLO: if ((ret = handle_server_hello(msg->hello, closure))) { if (sudo_timespecisset(closure->restart)) { closure->state = SEND_RESTART; ret = fmt_restart_message(closure); } else { closure->state = SEND_ACCEPT; ret = fmt_accept_message(closure); } } break; case SERVER_MESSAGE__TYPE_COMMIT_POINT: ret = handle_commit_point(msg->commit_point, closure); if (sudo_timespeccmp(closure->elapsed, &closure->committed, ==)) { sudo_ev_del(NULL, closure->read_ev); sudo_ev_loopexit(NULL); closure->state = FINISHED; } break; case SERVER_MESSAGE__TYPE_LOG_ID: ret = handle_log_id(msg->log_id, closure); break; case SERVER_MESSAGE__TYPE_ERROR: ret = handle_server_error(msg->error, closure); closure->state = ERROR; break; case SERVER_MESSAGE__TYPE_ABORT: ret = handle_server_abort(msg->abort, closure); closure->state = ERROR; break; default: sudo_warnx(U_("%s: unexpected type_case value %d"), __func__, msg->type_case); break; } server_message__free_unpacked(msg, NULL); debug_return_bool(ret); } /* * Read and unpack a ServerMessage (read callback). */ static void server_msg_cb(int fd, int what, void *v) { struct client_closure *closure = v; struct connection_buffer *buf = &closure->read_buf; ssize_t nread; uint32_t msg_len; debug_decl(server_msg_cb, SUDO_DEBUG_UTIL) sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage", __func__); /* XXX - make common */ nread = recv(fd, buf->data + buf->len, buf->size - buf->len, 0); sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from server", __func__, nread); switch (nread) { case -1: if (errno == EAGAIN) debug_return; sudo_warn("recv"); goto bad; case 0: sudo_warnx("%s", U_("premature EOF")); goto bad; default: break; } buf->len += nread; while (buf->len - buf->off >= sizeof(msg_len)) { /* Read wire message size (uint32_t in network byte order). */ memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len)); msg_len = ntohl(msg_len); if (msg_len > MESSAGE_SIZE_MAX) { sudo_warnx(U_("server message too large: %u\n"), msg_len); goto bad; } if (msg_len + sizeof(msg_len) > buf->len - buf->off) { /* Incomplete message, we'll read the rest next time. */ if (!expand_buf(buf, msg_len + sizeof(msg_len))) goto bad; debug_return; } /* Parse ServerMessage, could be zero bytes. */ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: parsing ServerMessage, size %u", __func__, msg_len); buf->off += sizeof(msg_len); if (!handle_server_message(buf->data + buf->off, msg_len, closure)) goto bad; buf->off += msg_len; } buf->len -= buf->off; buf->off = 0; debug_return; bad: close(fd); client_closure_free(closure); debug_return; } /* * Send a ClientMessage to the server (write callback). */ static void client_msg_cb(int fd, int what, void *v) { struct client_closure *closure = v; struct connection_buffer *buf = &closure->write_buf; ssize_t nwritten; debug_decl(client_msg_cb, SUDO_DEBUG_UTIL) sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to server", __func__, buf->len - buf->off); nwritten = send(fd, buf->data + buf->off, buf->len - buf->off, 0); if (nwritten == -1) { sudo_warn("send"); goto bad; } buf->off += nwritten; if (buf->off == buf->len) { /* sent entire message */ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: finished sending %u bytes to server", __func__, buf->len); buf->off = 0; buf->len = 0; if (!client_message_completion(closure)) goto bad; } debug_return; bad: close(fd); client_closure_free(closure); debug_return; } /* * Initialize a new client closure */ static bool client_closure_fill(struct client_closure *closure, int sock, struct timespec *elapsed, struct timespec *restart, const char *iolog_id, struct iolog_info *log_info) { debug_decl(client_closure_fill, SUDO_DEBUG_UTIL) memset(closure, 0, sizeof(*closure)); closure->state = RECV_HELLO; closure->log_info = log_info; closure->elapsed = elapsed; closure->restart = restart; closure->iolog_id = iolog_id; closure->bufsize = 8 * 1024; closure->buf = malloc(closure->bufsize); if (closure->buf == NULL) goto bad; closure->read_buf.size = 64 * 1024; closure->read_buf.data = malloc(closure->read_buf.size); if (closure->read_buf.data == NULL) goto bad; closure->read_ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST, server_msg_cb, closure); if (closure->read_ev == NULL) goto bad; closure->write_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE|SUDO_EV_PERSIST, client_msg_cb, closure); if (closure->write_ev == NULL) goto bad; debug_return_bool(true); bad: client_closure_free(closure); debug_return_bool(false); } #if 0 /* * Open the I/O log files and seek to the specified point in time. * TODO: share with logsrvd restart code */ static bool iolog_seekto(int iolog_dir_fd, const char *iolog_path, struct timespec *elapsed_time, const struct timespec *target) { struct timing_closure timing; off_t pos; int iofd; debug_decl(iolog_seekto, SUDO_DEBUG_UTIL) /* Open existing I/O log files. */ for (iofd = 0; iofd < IOFD_MAX; iofd++) { iolog_files[iofd].enabled = true; if (!iolog_open(&iolog_files[iofd], iolog_dir_fd, iofd, "r+")) { if (errno != ENOENT) { sudo_debug_printf( SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, "unable to open %s/%s", iolog_path, iolog_fd_to_name(iofd)); goto bad; } } } if (!iolog_files[IOFD_TIMING].enabled) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "missing timing file in %s", iolog_path); goto bad; } /* Parse timing file until we reach the target point. */ for (;;) { if (read_timing_record(&iolog_files[IOFD_TIMING], &timing) != 0) goto bad; sudo_timespecadd(&timing.delay, elapsed_time, elapsed_time); if (timing.event < IOFD_TIMING) { if (!iolog_files[timing.event].enabled) { /* Missing log file. */ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "iofd %d referenced but not open", timing.event); goto bad; } pos = iolog_seek(&iolog_files[timing.event], timing.u.nbytes, SEEK_CUR); if (pos == -1) { sudo_debug_printf( SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, "seek(%d, %lld, SEEK_CUR", timing.event, (long long)timing.u.nbytes); goto bad; } } if (sudo_timespeccmp(elapsed_time, target, >=)) { if (sudo_timespeccmp(elapsed_time, target, ==)) break; /* Mismatch between resume point and stored log. */ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "resume point mismatch, target [%lld, %ld], have [%lld, %ld]", (long long)target->tv_sec, target->tv_nsec, (long long)elapsed_time->tv_sec, elapsed_time->tv_nsec); goto bad; } } debug_return_bool(true); bad: debug_return_bool(false); } #endif /* * Parse a timespec on the command line of the form * seconds[,nanoseconds] */ static bool parse_timespec(struct timespec *ts, const char *strval) { long long tv_sec; long tv_nsec; char *ep; debug_decl(parse_timespec, SUDO_DEBUG_UTIL) errno = 0; tv_sec = strtoll(strval, &ep, 10); if (strval == ep || (*ep != ',' && *ep != '\0')) debug_return_bool(false); #if TIME_T_MAX != LLONG_MAX if (tv_sec > TIME_T_MAX) debug_return_bool(false); #endif if (tv_sec < 0 || (errno == ERANGE && tv_sec == LLONG_MAX)) debug_return_bool(false); strval = ep + 1; errno = 0; tv_nsec = strtol(strval, &ep, 10); if (strval == ep || *ep != '\0') debug_return_bool(false); if (tv_nsec < 0 || (errno == ERANGE && tv_nsec == LONG_MAX)) debug_return_bool(false); sudo_debug_printf(SUDO_DEBUG_INFO, "%s: parsed timespec [%lld, %ld]", __func__, tv_sec, tv_nsec); ts->tv_sec = (time_t)tv_sec; ts->tv_nsec = tv_nsec; debug_return_bool(true); } static const char short_opts[] = "h:i:p:r:V"; static struct option long_opts[] = { { "help", no_argument, NULL, 1 }, { "host", required_argument, NULL, 'h' }, { "iolog-id", required_argument, NULL, 'i' }, { "port", required_argument, NULL, 'p' }, { "restart", required_argument, NULL, 'r' }, { "version", no_argument, NULL, 'V' }, { NULL, no_argument, NULL, 0 }, }; __dso_public int main(int argc, char *argv[]); int main(int argc, char *argv[]) { struct client_closure closure; struct sudo_event_base *evbase; struct iolog_info *log_info; const char *host = "localhost"; const char *port = DEFAULT_PORT_STR; struct timespec restart = { 0, 0 }; struct timespec elapsed = { 0, 0 }; const char *iolog_id = NULL; const char *open_mode = "r"; int ch, sock, iolog_dir_fd, fd; FILE *fp; debug_decl_vars(main, SUDO_DEBUG_MAIN) #if defined(SUDO_DEVEL) && defined(__OpenBSD__) { extern char *malloc_options; malloc_options = "S"; } #endif signal(SIGPIPE, SIG_IGN); initprogname(argc > 0 ? argv[0] : "sudo_sendlog"); setlocale(LC_ALL, ""); bindtextdomain("sudo", LOCALEDIR); /* XXX - add logsrvd domain */ textdomain("sudo"); /* Read sudo.conf and initialize the debug subsystem. */ if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1) exit(EXIT_FAILURE); sudo_debug_register(getprogname(), NULL, NULL, sudo_conf_debug_files(getprogname())); if (protobuf_c_version_number() < 1003000) sudo_fatalx("%s", U_("Protobuf-C version 1.3 or higher required")); while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (ch) { case 'h': host = optarg; break; case 'i': iolog_id = optarg; break; case 'p': port = optarg; break; case 'r': if (!parse_timespec(&restart, optarg)) goto bad; open_mode = "r+"; break; case 1: help(); break; case 'V': (void)printf(_("%s version %s\n"), getprogname(), PACKAGE_VERSION); return 0; default: usage(true); } } argc -= optind; argv += optind; if (sudo_timespecisset(&restart) != (iolog_id != NULL)) { sudo_warnx("%s", U_("both restart point and iolog ID must be specified")); usage(true); } /* Remaining arg should be to I/O log dir to send. */ if (argc != 1) usage(true); iolog_dir = argv[0]; if ((iolog_dir_fd = open(iolog_dir, O_RDONLY)) == -1) { sudo_warn("%s", iolog_dir); goto bad; } /* Parse I/O log info file. */ fd = openat(iolog_dir_fd, "log", O_RDONLY, 0); if (fd == -1 || (fp = fdopen(fd, "r")) == NULL) { sudo_warn("%s/log", iolog_dir); goto bad; } if ((log_info = parse_logfile(fp, iolog_dir)) == NULL) goto bad; /* Open the I/O log files and seek to restart point if there is one. */ if (!iolog_open_all(iolog_dir_fd, iolog_dir, iolog_files, open_mode)) goto bad; if (sudo_timespecisset(&restart)) { if (!iolog_seekto(iolog_dir_fd, iolog_dir, iolog_files, &elapsed, &restart)) goto bad; } /* Connect to server, setup events. */ sock = connect_server(host, port); if (sock == -1) goto bad; printf("connected to %s:%s\n", host, port); if ((evbase = sudo_ev_base_alloc()) == NULL) sudo_fatal(NULL); sudo_ev_base_setdef(evbase); if (!client_closure_fill(&closure, sock, &elapsed, &restart, iolog_id, log_info)) goto bad; /* Add read event for the server hello message and enter event loop. */ if (sudo_ev_add(evbase, closure.read_ev, NULL, false) == -1) goto bad; sudo_ev_dispatch(evbase); if (closure.state != FINISHED) { sudo_warnx(U_("exited prematurely with state %d"), closure.state); sudo_warnx(U_("elapsed time sent to server [%lld, %ld]"), (long long)closure.elapsed->tv_sec, closure.elapsed->tv_nsec); sudo_warnx(U_("commit point received from server [%lld, %ld]"), (long long)closure.committed.tv_sec, closure.committed.tv_nsec); goto bad; } printf("I/O log transmitted successfully\n"); debug_return_int(EXIT_SUCCESS); bad: debug_return_int(EXIT_FAILURE); }