changeset 14607:43b841891c77

Added "connection" API for handling client/server connections more easily.
author Timo Sirainen <tss@iki.fi>
date Sun, 08 Jul 2012 08:59:52 +0300
parents 523c19238a8b
children 211fbc872ed4
files src/lib/Makefile.am src/lib/connection.c src/lib/connection.h
diffstat 3 files changed, 461 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/lib/Makefile.am	Sun Jul 08 07:45:17 2012 +0300
+++ b/src/lib/Makefile.am	Sun Jul 08 08:59:52 2012 +0300
@@ -22,6 +22,7 @@
 	child-wait.c \
 	close-keep-errno.c \
 	compat.c \
+	connection.c \
 	crc32.c \
 	data-stack.c \
 	eacces-error.c \
@@ -140,6 +141,7 @@
 	child-wait.h \
 	close-keep-errno.h \
 	compat.h \
+	connection.h \
 	crc32.h \
 	data-stack.h \
 	eacces-error.h \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/connection.c	Sun Jul 08 08:59:52 2012 +0300
@@ -0,0 +1,341 @@
+/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "network.h"
+#include "strescape.h"
+#include "llist.h"
+#include "connection.h"
+
+#include <unistd.h>
+
+static void connection_idle_timeout(struct connection *conn)
+{
+	conn->disconnect_reason = CONNECTION_DISCONNECT_IDLE_TIMEOUT;
+	conn->list->v.destroy(conn);
+}
+
+static void connection_connect_timeout(struct connection *conn)
+{
+	conn->disconnect_reason = CONNECTION_DISCONNECT_CONNECT_TIMEOUT;
+	conn->list->v.destroy(conn);
+}
+
+void connection_input_default(struct connection *conn)
+{
+	const char *line;
+	struct istream *input;
+	int ret = 0;
+
+	switch (connection_input_read(conn)) {
+	case -1:
+	case 0:
+		return;
+	case 1:
+		break;
+	default:
+		i_unreached();
+	}
+
+	input = conn->input;
+	i_stream_ref(input);
+	while (!input->closed && (line = i_stream_next_line(input)) != NULL) {
+		T_BEGIN {
+			ret = conn->list->v.input_line(conn, line);
+		} T_END;
+		if (ret <= 0)
+			break;
+	}
+	if (ret < 0 && !input->closed) {
+		conn->disconnect_reason = CONNECTION_DISCONNECT_DEINIT;
+		conn->list->v.destroy(conn);
+	}
+	i_stream_unref(&input);
+}
+
+int connection_verify_version(struct connection *conn,
+			      const char *const *args)
+{
+	unsigned int recv_major_version;
+
+	/* VERSION <tab> service_name <tab> major version <tab> minor version */
+	if (str_array_length(args) != 4 ||
+	    strcmp(args[0], "VERSION") != 0 ||
+	    str_to_uint(args[2], &recv_major_version) < 0 ||
+	    str_to_uint(args[3], &conn->minor_version) < 0) {
+		i_error("%s didn't reply with a valid VERSION line",
+			conn->name);
+		return -1;
+	}
+
+	if (strcmp(args[1], conn->list->set.service_name_in) != 0) {
+		i_error("%s: Connected to wrong socket type. "
+			"We want '%s', but received '%s'", conn->name,
+			conn->list->set.service_name_in, args[1]);
+		return -1;
+	}
+
+	if (recv_major_version != conn->list->set.major_version) {
+		i_error("%s: Socket supports major version %u, "
+			"but we support only %u (mixed old and new binaries?)",
+			conn->name, recv_major_version,
+			conn->list->set.major_version);
+		return -1;
+	}
+	return 0;
+}
+
+int connection_input_line_default(struct connection *conn, const char *line)
+{
+	const char *const *args;
+
+	args = t_strsplit_tabescaped(line);
+	if (!conn->version_received) {
+		if (connection_verify_version(conn, args) < 0)
+			return -1;
+		conn->version_received = TRUE;
+	}
+
+	return conn->list->v.input_args(conn, args);
+}
+
+static void connection_init_streams(struct connection *conn)
+{
+	const struct connection_settings *set = &conn->list->set;
+
+	i_assert(conn->input == NULL);
+	i_assert(conn->output == NULL);
+	i_assert(conn->to == NULL);
+
+	conn->version_received = set->major_version == 0;
+
+	if (set->input_max_size != 0) {
+		conn->input = i_stream_create_fd(conn->fd_in,
+						 set->input_max_size, FALSE);
+	}
+	if (set->output_max_size != 0) {
+		conn->output = o_stream_create_fd(conn->fd_out,
+						  set->output_max_size, FALSE);
+	}
+	if (set->input_idle_timeout_secs != 0) {
+		conn->to = timeout_add(set->input_idle_timeout_secs*1000,
+				       connection_idle_timeout, conn);
+	}
+	if (set->major_version != 0) {
+		o_stream_send_str(conn->output, t_strdup_printf(
+			"VERSION\t%s\t%u\t%u\n", set->service_name_out,
+			set->major_version, set->minor_version));
+	}
+}
+
+static void connection_init_io(struct connection *conn)
+{
+	i_assert(conn->io == NULL);
+	conn->io = io_add(conn->fd_in, IO_READ, conn->list->v.input, conn);
+}
+
+void connection_init_server(struct connection_list *list,
+			    struct connection *conn, const char *name,
+			    int fd_in, int fd_out)
+{
+	i_assert(name != NULL);
+	i_assert(!list->set.client);
+
+	conn->list = list;
+	conn->name = i_strdup(name);
+	conn->fd_in = fd_in;
+	conn->fd_out = fd_out;
+	connection_init_io(conn);
+	connection_init_streams(conn);
+
+	DLLIST_PREPEND(&list->connections, conn);
+}
+
+void connection_init_client_ip(struct connection_list *list,
+			       struct connection *conn,
+			       const struct ip_addr *ip, unsigned int port)
+{
+	i_assert(list->set.client);
+
+	conn->fd_in = conn->fd_out = -1;
+	conn->list = list;
+	conn->name = i_strdup_printf("%s:%u", net_ip2addr(ip), port);
+
+	conn->ip = *ip;
+	conn->port = port;
+
+	DLLIST_PREPEND(&list->connections, conn);
+}
+
+void connection_init_client_unix(struct connection_list *list,
+				 struct connection *conn, const char *path)
+{
+	i_assert(list->set.client);
+
+	conn->fd_in = conn->fd_out = -1;
+	conn->list = list;
+	conn->name = i_strdup(path);
+
+	DLLIST_PREPEND(&list->connections, conn);
+}
+
+static void connection_connected(struct connection *conn)
+{
+	io_remove(&conn->io);
+	if (conn->to != NULL)
+		timeout_remove(&conn->to);
+
+	connection_init_io(conn);
+}
+
+int connection_client_connect(struct connection *conn)
+{
+	const struct connection_settings *set = &conn->list->set;
+	int fd;
+
+	i_assert(conn->list->set.client);
+	i_assert(conn->fd_in == -1);
+
+	if (conn->port != 0)
+		fd = net_connect_ip(&conn->ip, conn->port, NULL);
+	else
+		fd = net_connect_unix(conn->name);
+	if (fd == -1)
+		return -1;
+	conn->fd_in = conn->fd_out = fd;
+
+	if (conn->port != 0) {
+		conn->io = io_add(conn->fd_out, IO_WRITE,
+				  connection_connected, conn);
+		if (set->client_connect_timeout_msecs != 0) {
+			conn->to = timeout_add(set->client_connect_timeout_msecs,
+					       connection_connect_timeout, conn);
+		}
+	} else {
+		connection_init_io(conn);
+	}
+	connection_init_streams(conn);
+	return 0;
+}
+
+void connection_disconnect(struct connection *conn)
+{
+	if (conn->to != NULL)
+		timeout_remove(&conn->to);
+	if (conn->io != NULL)
+		io_remove(&conn->io);
+	if (conn->input != NULL)
+		i_stream_destroy(&conn->input);
+	if (conn->output != NULL)
+		o_stream_destroy(&conn->output);
+	if (conn->fd_in != -1) {
+		if (close(conn->fd_in) < 0)
+			i_error("close(%s) failed: %m", conn->name);
+		if (conn->fd_in != conn->fd_out)
+			i_error("close(%s/out) failed: %m", conn->name);
+		conn->fd_in = conn->fd_out = -1;
+	}
+}
+
+void connection_deinit(struct connection *conn)
+{
+	DLLIST_REMOVE(&conn->list->connections, conn);
+
+	connection_disconnect(conn);
+	i_free(conn->name);
+}
+
+int connection_input_read(struct connection *conn)
+{
+	conn->last_input = ioloop_time;
+	if (conn->to != NULL)
+		timeout_reset(conn->to);
+
+	switch (i_stream_read(conn->input)) {
+	case -2:
+		/* buffer full */
+		switch (conn->list->set.input_full_behavior) {
+		case CONNECTION_BEHAVIOR_DESTROY:
+			conn->disconnect_reason =
+				CONNECTION_DISCONNECT_BUFFER_FULL;
+			conn->list->v.destroy(conn);
+			return -1;
+		case CONNECTION_BEHAVIOR_ALLOW:
+			return -2;
+		}
+	case -1:
+		/* disconnected */
+		conn->disconnect_reason =
+			CONNECTION_DISCONNECT_CONN_CLOSED;
+		conn->list->v.destroy(conn);
+		return -1;
+	case 0:
+		/* nothing new read */
+		return 0;
+	default:
+		/* something was read */
+		return 1;
+	}
+}
+
+const char *connection_disconnect_reason(struct connection *conn)
+{
+	if (conn->input != NULL && conn->input->stream_errno != 0)
+		errno = conn->input->stream_errno;
+	else if (conn->output != NULL && conn->output->stream_errno != 0)
+		errno = conn->output->stream_errno;
+
+	return errno == 0 || errno == EPIPE ? "Connection closed" :
+		t_strdup_printf("Connection closed: %m");
+}
+
+void connection_switch_ioloop(struct connection *conn)
+{
+	if (conn->io != NULL)
+		conn->io = io_loop_move_io(&conn->io);
+	if (conn->to != NULL)
+		conn->to = io_loop_move_timeout(&conn->to);
+	if (conn->output != NULL)
+		o_stream_switch_ioloop(conn->output);
+}
+
+struct connection_list *
+connection_list_init(const struct connection_settings *set,
+		     const struct connection_vfuncs *vfuncs)
+{
+	struct connection_list *list;
+
+	i_assert(vfuncs->input != NULL ||
+		 set->input_full_behavior != CONNECTION_BEHAVIOR_ALLOW);
+	i_assert(set->major_version == 0 ||
+		 (set->service_name_in != NULL &&
+		  set->service_name_out != NULL));
+
+	list = i_new(struct connection_list, 1);
+	list->set = *set;
+	list->v = *vfuncs;
+
+	if (list->v.input == NULL)
+		list->v.input = connection_input_default;
+	if (list->v.input_line == NULL)
+		list->v.input_line = connection_input_line_default;
+
+	return list;
+}
+
+void connection_list_deinit(struct connection_list **_list)
+{
+	struct connection_list *list = *_list;
+	struct connection *conn;
+
+	*_list = NULL;
+
+	while (list->connections != NULL) {
+		conn = list->connections;
+		list->v.destroy(conn);
+		i_assert(conn != list->connections);
+	}
+	i_free(list);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/connection.h	Sun Jul 08 08:59:52 2012 +0300
@@ -0,0 +1,118 @@
+#ifndef CONNECTION_H
+#define CONNECTION_H
+
+#include "network.h"
+
+struct connection;
+
+enum connection_behavior {
+	CONNECTION_BEHAVIOR_DESTROY = 0,
+	CONNECTION_BEHAVIOR_ALLOW
+};
+
+enum connection_disconnect_reason {
+	/* not disconnected yet */
+	CONNECTION_DISCONNECT_NOT = 0,
+	/* normal requested disconnection */
+	CONNECTION_DISCONNECT_DEINIT,
+	/* input buffer full */
+	CONNECTION_DISCONNECT_BUFFER_FULL,
+	/* connection got disconnected */
+	CONNECTION_DISCONNECT_CONN_CLOSED,
+	/* connect() timed out */
+	CONNECTION_DISCONNECT_CONNECT_TIMEOUT,
+	/* remote didn't send input */
+	CONNECTION_DISCONNECT_IDLE_TIMEOUT
+};
+
+struct connection_vfuncs {
+	void (*destroy)(struct connection *conn);
+
+	/* implement one of the input*() methods.
+	   They return 0 = ok, -1 = error, disconnect the client */
+	void (*input)(struct connection *conn);
+	int (*input_line)(struct connection *conn, const char *line);
+	int (*input_args)(struct connection *conn, const char *const *args);
+};
+
+struct connection_settings {
+	const char *service_name_in;
+	const char *service_name_out;
+	unsigned int major_version, minor_version;
+
+	unsigned int client_connect_timeout_msecs;
+	unsigned int input_idle_timeout_secs;
+
+	size_t input_max_size;
+	size_t output_max_size;
+	enum connection_behavior input_full_behavior;
+
+	bool client;
+};
+
+struct connection {
+	struct connection *prev, *next;
+	struct connection_list *list;
+
+	char *name;
+	int fd_in, fd_out;
+	struct io *io;
+	struct istream *input;
+	struct ostream *output;
+
+	struct timeout *to;
+	time_t last_input;
+
+	/* for IP client: */
+	struct ip_addr ip;
+	unsigned int port;
+
+	/* received minor version */
+	unsigned int minor_version;
+
+	enum connection_disconnect_reason disconnect_reason;
+
+	unsigned int version_received:1;
+};
+
+struct connection_list {
+	struct connection *connections;
+
+	struct connection_settings set;
+	struct connection_vfuncs v;
+};
+
+void connection_init_server(struct connection_list *list,
+			    struct connection *conn, const char *name,
+			    int fd_in, int fd_out);
+void connection_init_client_ip(struct connection_list *list,
+			       struct connection *conn,
+			       const struct ip_addr *ip, unsigned int port);
+void connection_init_client_unix(struct connection_list *list,
+				 struct connection *conn, const char *path);
+
+int connection_client_connect(struct connection *conn);
+
+void connection_disconnect(struct connection *conn);
+void connection_deinit(struct connection *conn);
+
+/* Returns -1 = disconnected, 0 = nothing new, 1 = something new.
+   If input_full_behavior is ALLOW, may return also -2 = buffer full. */
+int connection_input_read(struct connection *conn);
+/* Verify that VERSION input matches what we expect. */
+int connection_verify_version(struct connection *conn, const char *const *args);
+
+/* Returns human-readable reason for why connection was disconnected. */
+const char *connection_disconnect_reason(struct connection *conn);
+
+void connection_switch_ioloop(struct connection *conn);
+
+struct connection_list *
+connection_list_init(const struct connection_settings *set,
+		     const struct connection_vfuncs *vfuncs);
+void connection_list_deinit(struct connection_list **list);
+
+void connection_input_default(struct connection *conn);
+int connection_input_line_default(struct connection *conn, const char *line);
+
+#endif