diff options
Diffstat (limited to 'src/doveadm/client-connection-tcp.c')
-rw-r--r-- | src/doveadm/client-connection-tcp.c | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c new file mode 100644 index 0000000..b80d674 --- /dev/null +++ b/src/doveadm/client-connection-tcp.c @@ -0,0 +1,558 @@ +/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "lib-signals.h" +#include "str.h" +#include "base64.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "strescape.h" +#include "iostream-ssl.h" +#include "ostream-multiplex.h" +#include "master-service.h" +#include "master-service-ssl.h" +#include "mail-storage-service.h" +#include "doveadm-util.h" +#include "doveadm-mail.h" +#include "doveadm-print.h" +#include "doveadm-server.h" +#include "client-connection-private.h" + +#include <unistd.h> + +#define MAX_INBUF_SIZE (1024*1024) + +struct client_connection_tcp { + struct client_connection conn; + + int fd; + struct io *io; + struct istream *input; + struct ostream *output; + struct ostream *log_out; + struct ssl_iostream *ssl_iostream; + struct ioloop *ioloop; + + bool handshaked:1; + bool preauthenticated:1; + bool authenticated:1; + bool io_setup:1; + bool use_multiplex:1; +}; + +static void +client_connection_tcp_input(struct client_connection_tcp *conn); +static void +client_connection_tcp_send_auth_handshake(struct client_connection_tcp *conn); +static void +client_connection_tcp_destroy(struct client_connection_tcp **_conn); +static int +client_connection_tcp_init_ssl(struct client_connection_tcp *conn); + +static failure_callback_t *orig_error_callback, *orig_fatal_callback; +static failure_callback_t *orig_info_callback, *orig_debug_callback = NULL; + +static bool log_recursing = FALSE; + +static void ATTR_FORMAT(2, 0) +doveadm_server_log_handler(const struct failure_context *ctx, + const char *format, va_list args) +{ + struct client_connection_tcp *conn = NULL; + + if (doveadm_client != NULL && + doveadm_client->type == DOVEADM_CONNECTION_TYPE_TCP) + conn = (struct client_connection_tcp *)doveadm_client; + + if (!log_recursing && conn != NULL && + conn->log_out != NULL) T_BEGIN { + struct ioloop *prev_ioloop = current_ioloop; + struct ostream *log_out = conn->log_out; + char c; + const char *ptr; + bool corked; + va_list va; + + /* prevent re-entering this code if + any of the following code causes logging */ + log_recursing = TRUE; + + /* since we can get here from just about anywhere, make sure + the log ostream uses the connection's ioloop. */ + if (conn->ioloop != NULL) + io_loop_set_current(conn->ioloop); + + const char *log_prefix = + ctx->log_prefix != NULL ? ctx->log_prefix : + i_get_failure_prefix(); + size_t log_prefix_len = strlen(log_prefix); + c = doveadm_log_type_to_char(ctx->type); + corked = o_stream_is_corked(log_out); + + va_copy(va, args); + const char *str = t_strdup_vprintf(format, va); + va_end(va); + + if (!corked) + o_stream_cork(log_out); + for (;;) { + ptr = strchr(str, '\n'); + size_t len = ptr == NULL ? strlen(str) : + (size_t)(ptr - str); + + o_stream_nsend(log_out, &c, 1); + o_stream_nsend(log_out, log_prefix, log_prefix_len); + o_stream_nsend(log_out, str, len); + o_stream_nsend(log_out, "\n", 1); + + if (ptr == NULL) + break; + str = ptr+1; + } + o_stream_uncork(log_out); + if (corked) + o_stream_cork(log_out); + io_loop_set_current(prev_ioloop); + + log_recursing = FALSE; + } T_END; + + switch(ctx->type) { + case LOG_TYPE_DEBUG: + orig_debug_callback(ctx, format, args); + break; + case LOG_TYPE_INFO: + orig_info_callback(ctx, format, args); + break; + case LOG_TYPE_WARNING: + case LOG_TYPE_ERROR: + orig_error_callback(ctx, format, args); + break; + default: + i_unreached(); + } +} + +static void doveadm_server_capture_logs(void) +{ + i_assert(orig_debug_callback == NULL); + i_get_failure_handlers(&orig_fatal_callback, &orig_error_callback, + &orig_info_callback, &orig_debug_callback); + i_set_error_handler(doveadm_server_log_handler); + i_set_info_handler(doveadm_server_log_handler); + i_set_debug_handler(doveadm_server_log_handler); +} + +static void doveadm_server_restore_logs(void) +{ + i_assert(orig_debug_callback != NULL); + i_set_error_handler(orig_error_callback); + i_set_info_handler(orig_info_callback); + i_set_debug_handler(orig_debug_callback); + orig_fatal_callback = NULL; + orig_error_callback = NULL; + orig_info_callback = NULL; + orig_debug_callback = NULL; +} + +static void +doveadm_cmd_server_post(struct client_connection_tcp *conn, const char *cmd_name) +{ + const char *str = NULL; + + if (doveadm_exit_code == 0) { + o_stream_nsend(conn->output, "\n+\n", 3); + return; + } + + str = doveadm_exit_code_to_str(doveadm_exit_code); + + if (str != NULL) { + o_stream_nsend_str(conn->output, + t_strdup_printf("\n-%s\n", str)); + } else { + o_stream_nsend_str(conn->output, "\n-\n"); + i_error("BUG: Command '%s' returned unknown error code %d", + cmd_name, doveadm_exit_code); + } +} + +static void +doveadm_cmd_server_run_ver2(struct client_connection_tcp *conn, + int argc, const char *const argv[], + struct doveadm_cmd_context *cctx) +{ + i_getopt_reset(); + if (doveadm_cmd_run_ver2(argc, argv, cctx) < 0) + doveadm_exit_code = EX_USAGE; + doveadm_cmd_server_post(conn, cctx->cmd->name); +} + +static int doveadm_cmd_handle(struct client_connection_tcp *conn, + const char *cmd_name, + int argc, const char *const argv[], + struct doveadm_cmd_context *cctx) +{ + struct ioloop *prev_ioloop = current_ioloop; + const struct doveadm_cmd_ver2 *cmd_ver2; + + if ((cmd_ver2 = doveadm_cmd_find_with_args_ver2(cmd_name, &argc, &argv)) == NULL) { + i_error("doveadm: Client sent unknown command: %s", cmd_name); + return -1; + } + cctx->cmd = cmd_ver2; + + /* some commands will want to call io_loop_run(), but we're already + running one and we can't call the original one recursively, so + create a new ioloop. */ + conn->ioloop = io_loop_create(); + o_stream_switch_ioloop(conn->output); + if (conn->log_out != NULL) + o_stream_switch_ioloop(conn->log_out); + + doveadm_cmd_server_run_ver2(conn, argc, argv, cctx); + + o_stream_switch_ioloop_to(conn->output, prev_ioloop); + if (conn->log_out != NULL) + o_stream_switch_ioloop_to(conn->log_out, prev_ioloop); + io_loop_destroy(&conn->ioloop); + + /* clear all headers */ + doveadm_print_deinit(); + doveadm_print_init(DOVEADM_PRINT_TYPE_SERVER); + + /* We already sent the success/failure reply to the client. Return 0 + so caller never adds another failure reply. */ + return 0; +} + +static bool client_handle_command(struct client_connection_tcp *conn, + const char *const *args) +{ + struct doveadm_cmd_context cctx; + const char *flags, *cmd_name; + unsigned int argc = str_array_length(args); + + if (argc < 3) { + i_error("doveadm client: No command given"); + return FALSE; + } + i_zero(&cctx); + cctx.conn_type = conn->conn.type; + cctx.input = conn->input; + cctx.output = conn->output; + cctx.local_ip = conn->conn.local_ip; + cctx.remote_ip = conn->conn.remote_ip; + cctx.local_port = conn->conn.local_port; + cctx.remote_port = conn->conn.remote_port; + doveadm_exit_code = 0; + + flags = args[0]; + cctx.username = args[1]; + cmd_name = args[2]; + + doveadm_debug = FALSE; + doveadm_verbose = FALSE; + + for (; *flags != '\0'; flags++) { + switch (*flags) { + case 'D': + doveadm_debug = TRUE; + doveadm_verbose = TRUE; + break; + case 'v': + doveadm_verbose = TRUE; + break; + default: + i_error("doveadm client: Unknown flag: %c", *flags); + return FALSE; + } + } + + if (!doveadm_client_is_allowed_command(conn->conn.set, cmd_name)) { + i_error("doveadm client isn't allowed to use command: %s", + cmd_name); + return FALSE; + } + + client_connection_set_proctitle(&conn->conn, cmd_name); + o_stream_cork(conn->output); + /* Disable IO while running a command. This is required for commands + that do IO themselves (e.g. dsync-server). */ + io_remove(&conn->io); + if (doveadm_cmd_handle(conn, cmd_name, argc-2, args+2, &cctx) < 0) + o_stream_nsend(conn->output, "\n-\n", 3); + o_stream_uncork(conn->output); + conn->io = io_add_istream(conn->input, client_connection_tcp_input, conn); + client_connection_set_proctitle(&conn->conn, ""); + + /* Try to flush the output. It might finish later. */ + (void)o_stream_flush(conn->output); + return TRUE; +} + +static int +client_connection_tcp_authenticate(struct client_connection_tcp *conn) +{ + const struct doveadm_settings *set = conn->conn.set; + const char *line, *pass; + buffer_t *plain; + const unsigned char *data; + size_t size; + + if ((line = i_stream_read_next_line(conn->input)) == NULL) { + if (conn->input->eof) + return -1; + return 0; + } + + if (*set->doveadm_password == '\0') { + i_error("doveadm_password not set, " + "remote authentication disabled"); + return -1; + } + + if (strcmp(line, "STARTTLS") == 0) { + io_remove(&conn->io); + if (client_connection_tcp_init_ssl(conn) < 0) + return -1; + conn->io = io_add_istream(conn->input, client_connection_tcp_input, conn); + return 0; + } + + /* FIXME: some day we should probably let auth process do this and + support all kinds of authentication */ + if (!str_begins(line, "PLAIN\t")) { + i_error("doveadm client attempted non-PLAIN authentication: %s", line); + return -1; + } + + plain = t_buffer_create(128); + if (base64_decode(line + 6, strlen(line + 6), NULL, plain) < 0) { + i_error("doveadm client sent invalid base64 auth PLAIN data"); + return -1; + } + data = plain->data; + size = plain->used; + + if (size < 10 || data[0] != '\0' || + memcmp(data+1, "doveadm", 7) != 0 || data[8] != '\0') { + i_error("doveadm client didn't authenticate as 'doveadm'"); + return -1; + } + pass = t_strndup(data + 9, size - 9); + if (strlen(pass) != strlen(set->doveadm_password) || + !mem_equals_timing_safe(pass, set->doveadm_password, + strlen(pass))) { + i_error("doveadm client authenticated with wrong password"); + return -1; + } + return 1; +} + +static void client_log_disconnect_error(struct client_connection_tcp *conn) +{ + const char *error; + + error = conn->ssl_iostream == NULL ? NULL : + ssl_iostream_get_last_error(conn->ssl_iostream); + if (error == NULL) { + error = conn->input->stream_errno == 0 ? "EOF" : + strerror(conn->input->stream_errno); + } + i_error("doveadm client disconnected before handshake: %s", error); +} + +static void +client_connection_tcp_input(struct client_connection_tcp *conn) +{ + const char *line; + bool ok = TRUE; + int ret; + unsigned int minor; + + if (!conn->handshaked) { + if ((line = i_stream_read_next_line(conn->input)) == NULL) { + if (conn->input->eof || conn->input->stream_errno != 0) { + client_log_disconnect_error(conn); + client_connection_tcp_destroy(&conn); + } + return; + } + if (!version_string_verify_full(line, "doveadm-server", + DOVEADM_SERVER_PROTOCOL_VERSION_MAJOR, &minor)) { + i_error("doveadm client not compatible with this server " + "(mixed old and new binaries?)"); + client_connection_tcp_destroy(&conn); + return; + } + if (minor > 0) { + /* send version reply */ + o_stream_nsend_str(conn->output, + DOVEADM_CLIENT_PROTOCOL_VERSION_LINE"\n"); + conn->use_multiplex = TRUE; + } + client_connection_tcp_send_auth_handshake(conn); + conn->handshaked = TRUE; + } + if (!conn->authenticated) { + if ((ret = client_connection_tcp_authenticate(conn)) <= 0) { + if (ret < 0) { + o_stream_nsend(conn->output, "-\n", 2); + client_connection_tcp_destroy(&conn); + } + return; + } + o_stream_nsend(conn->output, "+\n", 2); + conn->authenticated = TRUE; + } + + if (!conn->io_setup) { + conn->io_setup = TRUE; + if (conn->use_multiplex) { + struct ostream *os = conn->output; + conn->output = o_stream_create_multiplex(os, SIZE_MAX); + o_stream_set_name(conn->output, o_stream_get_name(os)); + o_stream_set_no_error_handling(conn->output, TRUE); + o_stream_unref(&os); + conn->log_out = + o_stream_multiplex_add_channel(conn->output, + DOVEADM_LOG_CHANNEL_ID); + o_stream_set_no_error_handling(conn->log_out, TRUE); + o_stream_set_name(conn->log_out, t_strdup_printf("%s (log)", + o_stream_get_name(conn->output))); + doveadm_server_capture_logs(); + } + doveadm_print_ostream = conn->output; + } + + while (ok && !conn->input->closed && + (line = i_stream_read_next_line(conn->input)) != NULL) { + T_BEGIN { + const char *const *args; + + args = t_strsplit_tabescaped(line); + ok = client_handle_command(conn, args); + } T_END; + } + if (conn->input->eof || conn->input->stream_errno != 0 || !ok) + client_connection_tcp_destroy(&conn); +} + +static int +client_connection_tcp_init_ssl(struct client_connection_tcp *conn) +{ + const char *error; + + if (master_service_ssl_init(master_service, + &conn->input, &conn->output, + &conn->ssl_iostream, &error) < 0) { + i_error("SSL init failed: %s", error); + return -1; + } + if (ssl_iostream_handshake(conn->ssl_iostream) < 0) { + i_error("SSL handshake failed: %s", + ssl_iostream_get_last_error(conn->ssl_iostream)); + return -1; + } + return 0; +} + +static bool +client_connection_is_preauthenticated(int listen_fd) +{ + const char *listen_path; + struct stat st; + + /* we'll have to do this with stat(), because at least in Linux + fstat() always returns mode as 0777 */ + return net_getunixname(listen_fd, &listen_path) == 0 && + stat(listen_path, &st) == 0 && S_ISSOCK(st.st_mode) && + (st.st_mode & 0777) == 0600; +} + +static void +client_connection_tcp_send_auth_handshake(struct client_connection_tcp *conn) +{ + if (conn->preauthenticated) { + /* no need for client to authenticate */ + conn->authenticated = TRUE; + o_stream_nsend(conn->output, "+\n", 2); + } else { + o_stream_nsend(conn->output, "-\n", 2); + } +} + +static void +client_connection_tcp_free(struct client_connection *_conn) +{ + struct client_connection_tcp *conn = + (struct client_connection_tcp *)_conn; + + i_assert(_conn->type == DOVEADM_CONNECTION_TYPE_TCP); + + doveadm_print_deinit(); + doveadm_print_ostream = NULL; + + if (conn->log_out != NULL) { + doveadm_server_restore_logs(); + o_stream_unref(&conn->log_out); + } + ssl_iostream_destroy(&conn->ssl_iostream); + + io_remove(&conn->io); + o_stream_destroy(&conn->output); + i_stream_destroy(&conn->input); + i_close_fd(&conn->fd); +} + +struct client_connection * +client_connection_tcp_create(int fd, int listen_fd, bool ssl) +{ + struct client_connection_tcp *conn; + pool_t pool; + + pool = pool_alloconly_create("doveadm client", 1024*16); + conn = p_new(pool, struct client_connection_tcp, 1); + conn->fd = fd; + + if (client_connection_init(&conn->conn, + DOVEADM_CONNECTION_TYPE_TCP, pool, fd) < 0) { + client_connection_tcp_destroy(&conn); + return NULL; + } + conn->conn.free = client_connection_tcp_free; + + doveadm_print_init(DOVEADM_PRINT_TYPE_SERVER); + + conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE); + conn->output = o_stream_create_fd(fd, SIZE_MAX); + i_stream_set_name(conn->input, conn->conn.name); + o_stream_set_name(conn->output, conn->conn.name); + o_stream_set_no_error_handling(conn->output, TRUE); + + if (ssl) { + if (client_connection_tcp_init_ssl(conn) < 0) { + client_connection_tcp_destroy(&conn); + return NULL; + } + } + /* add IO after SSL istream is created */ + conn->io = io_add_istream(conn->input, client_connection_tcp_input, conn); + conn->preauthenticated = + client_connection_is_preauthenticated(listen_fd); + client_connection_set_proctitle(&conn->conn, ""); + + return &conn->conn; +} + +static void +client_connection_tcp_destroy(struct client_connection_tcp **_conn) +{ + struct client_connection_tcp *conn = *_conn; + struct client_connection *bconn = &conn->conn; + + *_conn = NULL; + client_connection_destroy(&bconn); +} |