summaryrefslogtreecommitdiffstats
path: root/src/imap/imap-client-hibernate.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/imap/imap-client-hibernate.c')
-rw-r--r--src/imap/imap-client-hibernate.c294
1 files changed, 294 insertions, 0 deletions
diff --git a/src/imap/imap-client-hibernate.c b/src/imap/imap-client-hibernate.c
new file mode 100644
index 0000000..b29f500
--- /dev/null
+++ b/src/imap/imap-client-hibernate.c
@@ -0,0 +1,294 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "fdpass.h"
+#include "net.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mailbox-watch.h"
+#include "imap-state.h"
+#include "imap-client.h"
+
+#include <sys/stat.h>
+
+#define IMAP_HIBERNATE_SOCKET_NAME "imap-hibernate"
+#define IMAP_HIBERNATE_SEND_TIMEOUT_SECS 10
+#define IMAP_HIBERNATE_HANDSHAKE "VERSION\timap-hibernate\t1\t0\n"
+
+static int
+imap_hibernate_handshake(int fd, const char *path, const char **error_r)
+{
+ char buf[1024];
+ ssize_t ret;
+
+ if (write_full(fd, IMAP_HIBERNATE_HANDSHAKE,
+ strlen(IMAP_HIBERNATE_HANDSHAKE)) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %m", path);
+ return -1;
+ } else if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %m", path);
+ return -1;
+ } else if (ret > 0 && buf[ret-1] == '\n') {
+ buf[ret-1] = '\0';
+ if (version_string_verify(buf, "imap-hibernate", 1))
+ return 0;
+ } else {
+ buf[ret] = '\0';
+ }
+ *error_r = t_strdup_printf("%s sent invalid VERSION handshake: %s",
+ path, buf);
+ return -1;
+}
+
+static void imap_hibernate_write_cmd(struct client *client, string_t *cmd,
+ const buffer_t *state, int fd_notify)
+{
+ struct mail_user *user = client->user;
+ struct stat peer_st;
+ const char *tag;
+
+ tag = client->command_queue == NULL ? NULL : client->command_queue->tag;
+
+ str_append_tabescaped(cmd, user->username);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, user->set->mail_log_prefix);
+ str_printfa(cmd, "\tidle_notify_interval=%u",
+ client->set->imap_idle_notify_interval);
+ if (fstat(client->fd_in, &peer_st) == 0) {
+ str_printfa(cmd, "\tpeer_dev_major=%lu\tpeer_dev_minor=%lu\tpeer_ino=%llu",
+ (unsigned long)major(peer_st.st_dev),
+ (unsigned long)minor(peer_st.st_dev),
+ (unsigned long long)peer_st.st_ino);
+ }
+
+ str_append(cmd, "\tsession=");
+ str_append_tabescaped(cmd, user->session_id);
+ if (user->session_create_time != 0) {
+ str_printfa(cmd, "\tsession_created=%s",
+ dec2str(user->session_create_time));
+ }
+ if (user->conn.local_ip != NULL)
+ str_printfa(cmd, "\tlip=%s", net_ip2addr(user->conn.local_ip));
+ if (user->conn.local_port != 0)
+ str_printfa(cmd, "\tlport=%u", user->conn.local_port);
+ if (user->conn.remote_ip != NULL)
+ str_printfa(cmd, "\trip=%s", net_ip2addr(user->conn.remote_ip));
+ if (user->conn.remote_port != 0)
+ str_printfa(cmd, "\trport=%u", user->conn.remote_port);
+ if (client->userdb_fields != NULL) {
+ string_t *userdb_fields = t_str_new(256);
+ unsigned int i;
+
+ for (i = 0; client->userdb_fields[i] != NULL; i++) {
+ if (i > 0)
+ str_append_c(userdb_fields, '\t');
+ str_append_tabescaped(userdb_fields, client->userdb_fields[i]);
+ }
+ str_append(cmd, "\tuserdb_fields=");
+ str_append_tabescaped(cmd, str_c(userdb_fields));
+ }
+ if (user->uid != (uid_t)-1)
+ str_printfa(cmd, "\tuid=%s", dec2str(user->uid));
+ if (user->gid != (gid_t)-1)
+ str_printfa(cmd, "\tgid=%s", dec2str(user->gid));
+ if (client->mailbox != NULL) {
+ str_append(cmd, "\tmailbox=");
+ str_append_tabescaped(cmd, mailbox_get_vname(client->mailbox));
+ }
+ if (tag != NULL) {
+ str_append(cmd, "\ttag=");
+ str_append_tabescaped(cmd, tag);
+ }
+ str_append(cmd, "\tstats=");
+ str_append_tabescaped(cmd, client_stats(client));
+ if (client->command_queue != NULL &&
+ strcasecmp(client->command_queue->name, "IDLE") == 0)
+ str_append(cmd, "\tidle-cmd");
+ if (fd_notify != -1)
+ str_append(cmd, "\tnotify_fd");
+ str_append(cmd, "\tstate=");
+ base64_encode(state->data, state->used, cmd);
+ str_append_c(cmd, '\n');
+}
+
+static int
+imap_hibernate_process_send_cmd(int fd_socket, const char *path,
+ const string_t *cmd, int fd_client,
+ const char **error_r)
+{
+ ssize_t ret;
+
+ i_assert(fd_socket != -1);
+ i_assert(str_len(cmd) > 1);
+
+ if (imap_hibernate_handshake(fd_socket, path, error_r) < 0)
+ return -1;
+ if ((ret = fd_send(fd_socket, fd_client, str_data(cmd), 1)) < 0) {
+ *error_r = t_strdup_printf("fd_send(%s) failed: %m", path);
+ return -1;
+ }
+ i_assert(ret == 1);
+ if (write_full(fd_socket, str_data(cmd)+1, str_len(cmd)-1) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imap_hibernate_process_read(int fd, const char *path, const char **error_r)
+{
+ char buf[1024];
+ ssize_t ret;
+
+ if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %m", path);
+ return -1;
+ } else if (ret == 0) {
+ *error_r = t_strdup_printf("%s disconnected", path);
+ return -1;
+ } else if (buf[0] != '+') {
+ buf[ret] = '\0';
+ *error_r = t_strdup_printf("%s returned failure: %s", path,
+ ret > 0 && buf[0] == '-' ? buf+1 : buf);
+ return -1;
+ } else {
+ return 0;
+ }
+}
+
+static int
+imap_hibernate_process_send(struct client *client, const buffer_t *state,
+ int fd_notify, int *fd_r, const char **error_r)
+{
+ string_t *cmd = t_str_new(512);
+ const char *path;
+ ssize_t ret = 0;
+ int fd;
+
+ i_assert(state->used > 0);
+
+ *fd_r = -1;
+
+ path = t_strconcat(client->user->set->base_dir,
+ "/"IMAP_HIBERNATE_SOCKET_NAME, NULL);
+ fd = net_connect_unix_with_retries(path, 1000);
+ if (fd == -1) {
+ *error_r = t_strdup_printf(
+ "net_connect_unix(%s) failed: %m", path);
+ return -1;
+ }
+ net_set_nonblock(fd, FALSE);
+
+ imap_hibernate_write_cmd(client, cmd, state, fd_notify);
+
+ alarm(IMAP_HIBERNATE_SEND_TIMEOUT_SECS);
+ if (imap_hibernate_process_send_cmd(fd, path, cmd, client->fd_in, error_r) < 0 ||
+ imap_hibernate_process_read(fd, path, error_r) < 0)
+ ret = -1;
+ else if (fd_notify != -1) {
+ if ((ret = fd_send(fd, fd_notify, "\n", 1)) < 0)
+ *error_r = t_strdup_printf("fd_send(%s) failed: %m", path);
+ else
+ ret = imap_hibernate_process_read(fd, path, error_r);
+ }
+ alarm(0);
+ if (ret < 0) {
+ net_disconnect(fd);
+ return -1;
+ }
+ *fd_r = fd;
+ return 0;
+}
+
+bool imap_client_hibernate(struct client **_client, const char **reason_r)
+{
+ struct client *client = *_client;
+ buffer_t *state;
+ const char *error;
+ int ret, fd_notify = -1, fd_hibernate = -1;
+
+ *reason_r = NULL;
+
+ if (client->fd_in != client->fd_out) {
+ /* we won't try to hibernate stdio clients */
+ *reason_r = "stdio clients can't be hibernated";
+ return FALSE;
+ }
+ if (o_stream_get_buffer_used_size(client->output) > 0) {
+ /* wait until we've sent the pending output to client */
+ *reason_r = "output pending to client";
+ return FALSE;
+ }
+
+ struct event_passthrough *e =
+ event_create_passthrough(client->event)->
+ set_name("imap_client_hibernated");
+ if (client->mailbox != NULL)
+ e->add_str("mailbox", mailbox_get_vname(client->mailbox));
+
+ state = buffer_create_dynamic(default_pool, 1024);
+ ret = imap_state_export_internal(client, state, &error);
+ if (ret < 0) {
+ e->add_str("error", error);
+ e_error(e->event(), "Couldn't hibernate imap client: "
+ "Couldn't export state: %s (mailbox=%s)", error,
+ client->mailbox == NULL ? "" :
+ mailbox_get_vname(client->mailbox));
+ *reason_r = error;
+ } else if (ret == 0) {
+ e->add_str("error", error);
+ e_debug(e->event(), "Couldn't hibernate imap client: "
+ "Couldn't export state: %s (mailbox=%s)", error,
+ client->mailbox == NULL ? "" :
+ mailbox_get_vname(client->mailbox));
+ *reason_r = error;
+ }
+ if (ret > 0 && client->mailbox != NULL) {
+ fd_notify = mailbox_watch_extract_notify_fd(client->mailbox,
+ &error);
+ if (fd_notify == -1) {
+ e->add_str("error", error);
+ e_debug(e->event(), "Couldn't hibernate imap client: "
+ "Couldn't extract notifications fd: %s",
+ error);
+ *reason_r = error;
+ ret = -1;
+ }
+ }
+ if (ret > 0) {
+ if (imap_hibernate_process_send(client, state, fd_notify,
+ &fd_hibernate, &error) < 0) {
+ e->add_str("error", error);
+ e_error(e->event(),
+ "Couldn't hibernate imap client: %s", error);
+ *reason_r = error;
+ ret = -1;
+ }
+ }
+ i_close_fd(&fd_notify);
+ if (ret > 0) {
+ /* hide the disconnect log message, because the client didn't
+ actually log out */
+ e_debug(e->event(),
+ "Successfully hibernated imap client in mailbox %s",
+ client->mailbox == NULL ? "<none>" :
+ mailbox_get_vname(client->mailbox));
+ client->disconnected = TRUE;
+ client->hibernated = TRUE;
+ client_destroy(client, NULL);
+ *_client = NULL;
+ }
+ /* notify imap-hibernate that we're done by closing the connection.
+ do this only after client is destroyed. this way imap-hibernate
+ won't try to launch another imap process too early and cause
+ problems (like sending duplicate session ID to stats process) */
+ if (fd_hibernate != -1)
+ net_disconnect(fd_hibernate);
+ buffer_free(&state);
+ return ret > 0;
+}