summaryrefslogtreecommitdiffstats
path: root/src/doveadm/client-connection-tcp.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/doveadm/client-connection-tcp.c')
-rw-r--r--src/doveadm/client-connection-tcp.c558
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);
+}