/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "array.h"
#include "hash.h"
#include "hostpid.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "net.h"
#include "strescape.h"
#include "eacces-error.h"
#include "auth-client-private.h"

#include <unistd.h>

#define AUTH_SERVER_CONN_MAX_LINE_LENGTH AUTH_CLIENT_MAX_LINE_LENGTH
#define AUTH_SERVER_RECONNECT_TIMEOUT_SECS 5

static void auth_client_connection_connected(struct connection *_conn,
					     bool success);
static int
auth_client_connection_input_line(struct connection *_conn,
				  const char *line);
static int
auth_client_connection_handshake_line(struct connection *_conn,
				      const char *line);
static void auth_client_connection_handshake_ready(struct connection *_conn);
static void auth_client_connection_destroy(struct connection *_conn);
static void
auth_client_connection_reconnect(struct auth_client_connection *conn,
				 const char *disconnect_reason);

static const struct connection_vfuncs auth_client_connection_vfuncs = {
	.destroy = auth_client_connection_destroy,
	.handshake_line = auth_client_connection_handshake_line,
	.handshake_ready = auth_client_connection_handshake_ready,
	.input_line = auth_client_connection_input_line,
	.client_connected = auth_client_connection_connected,
};

static const struct connection_settings auth_client_connection_set = {
	.dont_send_version = TRUE,
	.service_name_in = "auth-client",
	.service_name_out = "auth-client",
	.major_version = AUTH_CLIENT_PROTOCOL_MAJOR_VERSION,
	.minor_version = AUTH_CLIENT_PROTOCOL_MINOR_VERSION,
	.unix_client_connect_msecs = 1000,
	.input_max_size = AUTH_SERVER_CONN_MAX_LINE_LENGTH,
	.output_max_size = SIZE_MAX,
	.client = TRUE,
};

struct connection_list *
auth_client_connection_list_init(void)
{
	return connection_list_init(&auth_client_connection_set,
				    &auth_client_connection_vfuncs);
}

static int
auth_server_input_mech(struct auth_client_connection *conn,
		       const char *const *args)
{
	struct auth_mech_desc mech_desc;

	if (args[0] == NULL) {
		e_error(conn->conn.event,
			"BUG: Authentication server sent broken MECH line");
		return -1;
	}

	i_zero(&mech_desc);
	mech_desc.name = p_strdup(conn->pool, args[0]);

	if (strcmp(mech_desc.name, SASL_MECH_NAME_PLAIN) == 0)
		conn->has_plain_mech = TRUE;

	for (args++; *args != NULL; args++) {
		if (strcmp(*args, "private") == 0)
			mech_desc.flags |= SASL_MECH_SEC_PRIVATE;
		else if (strcmp(*args, "anonymous") == 0)
			mech_desc.flags |= SASL_MECH_SEC_ANONYMOUS;
		else if (strcmp(*args, "plaintext") == 0)
			mech_desc.flags |= SASL_MECH_SEC_PLAINTEXT;
		else if (strcmp(*args, "dictionary") == 0)
			mech_desc.flags |= SASL_MECH_SEC_DICTIONARY;
		else if (strcmp(*args, "active") == 0)
			mech_desc.flags |= SASL_MECH_SEC_ACTIVE;
		else if (strcmp(*args, "forward-secrecy") == 0)
			mech_desc.flags |= SASL_MECH_SEC_FORWARD_SECRECY;
		else if (strcmp(*args, "mutual-auth") == 0)
			mech_desc.flags |= SASL_MECH_SEC_MUTUAL_AUTH;
		else if (strcmp(*args, "channel-binding") == 0)
			mech_desc.flags |= SASL_MECH_SEC_CHANNEL_BINDING;
	}
	array_push_back(&conn->available_auth_mechs, &mech_desc);
	return 0;
}

static int
auth_server_input_spid(struct auth_client_connection *conn,
		       const char *const *args)
{
	if (str_to_uint(args[0], &conn->server_pid) < 0) {
		e_error(conn->conn.event,
			"BUG: Authentication server sent invalid PID");
		return -1;
	}
	return 0;
}

static int
auth_server_input_cuid(struct auth_client_connection *conn,
		       const char *const *args)
{
	if (args[0] == NULL ||
	    str_to_uint(args[0], &conn->connect_uid) < 0) {
		e_error(conn->conn.event,
			"BUG: Authentication server sent broken CUID line");
		return -1;
	}
	return 0;
}

static int
auth_server_input_cookie(struct auth_client_connection *conn,
			 const char *const *args)
{
	if (conn->cookie != NULL) {
		e_error(conn->conn.event,
			"BUG: Authentication server already sent cookie");
		return -1;
	}
	conn->cookie = p_strdup(conn->pool, args[0]);
	return 0;
}

static int auth_server_input_done(struct auth_client_connection *conn)
{
	if (array_count(&conn->available_auth_mechs) == 0) {
		e_error(conn->conn.event,
			"BUG: Authentication server returned no mechanisms");
		return -1;
	}
	if (conn->cookie == NULL) {
		e_error(conn->conn.event,
			"BUG: Authentication server didn't send a cookie");
		return -1;
	}
	return 1;
}

static int
auth_client_connection_handshake_line(struct connection *_conn,
				      const char *line)
{
	struct auth_client_connection *conn =
		container_of(_conn, struct auth_client_connection, conn);
	unsigned int major_version, minor_version;
	const char *const *args;

	args = t_strsplit_tabescaped(line);
	if (strcmp(args[0], "VERSION") == 0 &&
	    args[1] != NULL && args[2] != NULL) {
		if (str_to_uint(args[1], &major_version) < 0 ||
		    str_to_uint(args[2], &minor_version) < 0) {
			e_error(conn->conn.event,
				"Auth server sent invalid version line: %s",
				line);
			return -1;
		}

		if (connection_verify_version(_conn, "auth-client",
					      major_version,
					      minor_version) < 0) {
			return -1;
		}

		return 0;
	} else if (strcmp(args[0], "MECH") == 0) {
		return auth_server_input_mech(conn, args + 1);
	} else if (strcmp(args[0], "SPID") == 0) {
		return auth_server_input_spid(conn, args + 1);
	} else if (strcmp(args[0], "CUID") == 0) {
		return auth_server_input_cuid(conn, args + 1);
	} else if (strcmp(args[0], "COOKIE") == 0) {
		return auth_server_input_cookie(conn, args + 1);
	} else if (strcmp(args[0], "DONE") == 0) {
		return auth_server_input_done(conn);
	}

	e_error(conn->conn.event,
		"Auth server sent unknown handshake: %s", line);
	return -1;
}

static void auth_client_connection_handshake_ready(struct connection *_conn)
{
	struct auth_client_connection *conn =
		container_of(_conn, struct auth_client_connection, conn);

	struct auth_mech_desc *mech_desc;
	array_foreach_modifiable(&conn->client->available_auth_mechs,
				 mech_desc) {
		i_free(mech_desc->name);
	}
	array_clear(&conn->client->available_auth_mechs);
	array_foreach_modifiable(&conn->available_auth_mechs, mech_desc) {
		struct auth_mech_desc *dup_desc =
			array_append_space(&conn->client->available_auth_mechs);
		*dup_desc = *mech_desc;
		dup_desc->name = i_strdup(mech_desc->name);
	}

	timeout_remove(&conn->to);
	if (conn->client->connect_notify_callback != NULL) {
		conn->client->connect_notify_callback(conn->client, NULL,
				conn->client->connect_notify_context);
	}
}

static int
auth_server_lookup_request(struct auth_client_connection *conn,
			   const char *id_arg, bool server_finished,
			   struct auth_client_request **request_r)
{
	struct auth_client_request *request;
	unsigned int id;

	if (id_arg == NULL || str_to_uint(id_arg, &id) < 0) {
		e_error(conn->conn.event,
			"BUG: Authentication server input missing ID");
		return -1;
	}

	request = hash_table_lookup(conn->requests, POINTER_CAST(id));
	if (request == NULL) {
		e_error(conn->conn.event,
			"Authentication server sent unknown id %u", id);
		return 0;
	}
	if (request->server_finished) {
		e_error(conn->conn.event,
			"Authentication server sent finished id %u", id);
		return 0;
	}
	if (server_finished)
		request->server_finished = TRUE;

	*request_r = request;
	return 1;
}

static int
auth_server_input_ok(struct auth_client_connection *conn,
		     const char *const *args)
{
	struct auth_client_request *request;
	int ret;

	ret = auth_server_lookup_request(conn, args[0], TRUE, &request);
	if (ret <= 0)
		return ret;
	auth_client_request_server_input(&request, AUTH_REQUEST_STATUS_OK,
					 args + 1);
	return 0;
}

static int
auth_server_input_cont(struct auth_client_connection *conn,
		       const char *const *args)
{
	struct auth_client_request *request;
	int ret;

	if (str_array_length(args) < 2) {
		e_error(conn->conn.event,
			"BUG: Authentication server sent broken CONT line");
		return -1;
	}

	ret = auth_server_lookup_request(conn, args[0], FALSE, &request);
	if (ret <= 0)
		return ret;
	auth_client_request_server_input(&request, AUTH_REQUEST_STATUS_CONTINUE,
					 args + 1);
	return 0;
}

static int
auth_server_input_fail(struct auth_client_connection *conn,
		       const char *const *args)
{
	struct auth_client_request *request;
	int ret;

	ret = auth_server_lookup_request(conn, args[0], TRUE, &request);
	if (ret <= 0)
		return ret;
	auth_client_request_server_input(&request, AUTH_REQUEST_STATUS_FAIL,
					 args + 1);
	return 0;
}

static int
auth_server_input_cancelled(struct auth_client_connection *conn,
			    const char *const *args)
{
	unsigned int id;

	if (args[0] == NULL || str_to_uint(args[0], &id) < 0) {
		e_error(conn->conn.event,
			"BUG: Authentication server input missing ID");
		return -1;
	}

	struct auth_client_request *request =
		hash_table_lookup(conn->requests, POINTER_CAST(id));
	if (request != NULL && !request->server_finished) {
		request->server_finished = TRUE;
		auth_client_request_server_input(&request,
			AUTH_REQUEST_STATUS_FAIL, args + 1);
	}
	return 0;
}

static int
auth_client_connection_handle_line(struct auth_client_connection *conn,
				   const char *line)
{
	const char *const *args;

	e_debug(conn->conn.event, "auth input: %s", line);

	args = t_strsplit_tabescaped(line);
	if (args[0] == NULL) {
		e_error(conn->conn.event, "Auth server sent empty line");
		return -1;
	}
	if (strcmp(args[0], "OK") == 0)
		return auth_server_input_ok(conn, args + 1);
	else if (strcmp(args[0], "CONT") == 0)
		return auth_server_input_cont(conn, args + 1);
	else if (strcmp(args[0], "FAIL") == 0)
		return auth_server_input_fail(conn, args + 1);
	else if (strcmp(args[0], "CANCELLED") == 0)
		return auth_server_input_cancelled(conn, args + 1);
	else {
		e_error(conn->conn.event,
			"Auth server sent unknown response: %s", args[0]);
		return -1;
	}
}

static int
auth_client_connection_input_line(struct connection *_conn,
				  const char *line)
{
	struct auth_client_connection *conn =
		container_of(_conn, struct auth_client_connection, conn);
	int ret;

	ret = auth_client_connection_handle_line(conn, line);
	if (ret < 0) {
		auth_client_connection_disconnect(conn, t_strdup_printf(
			"Received broken input: %s", line));
		return -1;
	}
	return 1;
}

struct auth_client_connection *
auth_client_connection_init(struct auth_client *client)
{
	struct auth_client_connection *conn;
	pool_t pool;

	pool = pool_alloconly_create("auth server connection", 1024);
	conn = p_new(pool, struct auth_client_connection, 1);
	conn->pool = pool;

	conn->client = client;

	conn->conn.event_parent = client->event;
	connection_init_client_unix(client->clist, &conn->conn,
				    client->auth_socket_path);

	hash_table_create_direct(&conn->requests, pool, 100);
	i_array_init(&conn->available_auth_mechs, 8);
	return conn;
}

static void
auth_client_connection_remove_requests(struct auth_client_connection *conn,
				       const char *disconnect_reason)
{
	static const char *const temp_failure_args[] = {
		"code="AUTH_CLIENT_FAIL_CODE_TEMPFAIL,
		NULL
	};
	struct hash_iterate_context *iter;
	void *key;
	struct auth_client_request *request;
	time_t created, oldest = 0;
	unsigned int request_count = 0;

	if (hash_table_count(conn->requests) == 0)
		return;

	iter = hash_table_iterate_init(conn->requests);
	while (hash_table_iterate(iter, conn->requests, &key, &request)) {
		if (!auth_client_request_is_aborted(request)) {
			request_count++;
			created = auth_client_request_get_create_time(request);
			if (oldest > created || oldest == 0)
				oldest = created;
		}

		auth_client_request_server_input(&request,
			AUTH_REQUEST_STATUS_INTERNAL_FAIL,
			temp_failure_args);
	}
	hash_table_iterate_deinit(&iter);
	hash_table_clear(conn->requests, FALSE);

	if (request_count > 0) {
		e_warning(conn->conn.event,
			  "Auth connection closed with %u pending requests "
			  "(max %u secs, pid=%s, %s)", request_count,
			  (unsigned int)(ioloop_time - oldest),
			  my_pid, disconnect_reason);
	}
}

void auth_client_connection_disconnect(struct auth_client_connection *conn,
				       const char *reason)
{
	if (reason == NULL)
		reason = "Disconnected from auth server, aborting";

	if (conn->connected)
		connection_disconnect(&conn->conn);
	conn->connected = FALSE;

	conn->has_plain_mech = FALSE;
	conn->server_pid = 0;
	conn->connect_uid = 0;
	conn->cookie = NULL;
	array_clear(&conn->available_auth_mechs);

	timeout_remove(&conn->to);

	auth_client_connection_remove_requests(conn, reason);

	if (conn->client->connect_notify_callback != NULL) {
		conn->client->connect_notify_callback(conn->client, reason,
				conn->client->connect_notify_context);
	}
}

static void auth_client_connection_destroy(struct connection *_conn)
{
	struct auth_client_connection *conn =
		container_of(_conn, struct auth_client_connection, conn);

	switch (_conn->disconnect_reason) {
	case CONNECTION_DISCONNECT_HANDSHAKE_FAILED:
		auth_client_connection_disconnect(
			conn, "Handshake with auth service failed");
		break;
	case CONNECTION_DISCONNECT_BUFFER_FULL:
		/* buffer full - can't happen unless auth is buggy */
		e_error(conn->conn.event,
			"BUG: Auth server sent us more than %d bytes of data",
			AUTH_SERVER_CONN_MAX_LINE_LENGTH);
		auth_client_connection_disconnect(conn, "Buffer full");
		break;
	default:
		/* disconnected */
		auth_client_connection_reconnect(
			conn, (conn->conn.input->stream_errno != 0 ?
			       strerror(conn->conn.input->stream_errno) :
			       "EOF"));
	}
}

void auth_server_reconnect_timeout(struct auth_client_connection *conn)
{
	(void)auth_client_connection_connect(conn);
}

static void
auth_client_connection_reconnect(struct auth_client_connection *conn,
				 const char *disconnect_reason)
{
	time_t next_connect;

	auth_client_connection_disconnect(conn, disconnect_reason);

	next_connect = conn->last_connect + AUTH_SERVER_RECONNECT_TIMEOUT_SECS;
	conn->to = timeout_add(ioloop_time >= next_connect ? 0 :
			       (next_connect - ioloop_time) * 1000,
			       auth_server_reconnect_timeout, conn);
}

void auth_client_connection_deinit(struct auth_client_connection **_conn)
{
        struct auth_client_connection *conn = *_conn;

	if (conn == NULL)
		return;
	*_conn = NULL;

	auth_client_connection_disconnect(conn, "deinitializing");
	i_assert(hash_table_count(conn->requests) == 0);
	hash_table_destroy(&conn->requests);
	timeout_remove(&conn->to);
	array_free(&conn->available_auth_mechs);
	connection_deinit(&conn->conn);
	pool_unref(&conn->pool);
}

static void auth_client_handshake_timeout(struct auth_client_connection *conn)
{
	e_error(conn->conn.event,
		"Timeout waiting for handshake from auth server. "
		"my pid=%u, input bytes=%"PRIuUOFF_T,
		conn->client->client_pid, conn->conn.input->v_offset);
	auth_client_connection_reconnect(conn, "auth server timeout");
}

static void
auth_client_connection_connected(struct connection *_conn, bool success)
{
	struct auth_client_connection *conn =
		container_of(_conn, struct auth_client_connection, conn);

	/* Cannot get here unless connect() was successful */
	i_assert(success);

	conn->connected = TRUE;
}

int auth_client_connection_connect(struct auth_client_connection *conn)
{
	const char *handshake;

	i_assert(!conn->connected);

	conn->last_connect = ioloop_time;
	timeout_remove(&conn->to);

	/* max. 1 second wait here. */
	if (connection_client_connect(&conn->conn) < 0) {
		if (errno == EACCES) {
			e_error(conn->conn.event, "%s",
				eacces_error_get("connect",
					conn->client->auth_socket_path));
		} else {
			e_error(conn->conn.event, "connect(%s) failed: %m",
				conn->client->auth_socket_path);
		};
		return -1;
	}

	handshake = t_strdup_printf("VERSION\t%u\t%u\nCPID\t%u\n",
				    AUTH_CLIENT_PROTOCOL_MAJOR_VERSION,
                                    AUTH_CLIENT_PROTOCOL_MINOR_VERSION,
				    conn->client->client_pid);
	if (o_stream_send_str(conn->conn.output, handshake) < 0) {
		e_warning(conn->conn.event,
			  "Error sending handshake to auth server: %s",
			  o_stream_get_error(conn->conn.output));
		auth_client_connection_disconnect(conn,
			o_stream_get_error(conn->conn.output));
		return -1;
	}

	conn->to = timeout_add(conn->client->connect_timeout_msecs,
			       auth_client_handshake_timeout, conn);
	return 0;
}

unsigned int
auth_client_connection_add_request(struct auth_client_connection *conn,
				   struct auth_client_request *request)
{
	unsigned int id;

	i_assert(connection_handshake_received(&conn->conn));

	id = ++conn->client->request_id_counter;
	if (id == 0) {
		/* wrapped - ID 0 not allowed */
		id = ++conn->client->request_id_counter;
	}
	i_assert(hash_table_lookup(conn->requests, POINTER_CAST(id)) == NULL);
	hash_table_insert(conn->requests, POINTER_CAST(id), request);
	return id;
}

bool auth_client_connection_remove_request(struct auth_client_connection *conn,
					   struct auth_client_request *request)
{
	if (request->removed)
		return TRUE;
	i_assert(connection_handshake_received(&conn->conn));
	if (request->sent && !request->server_finished && conn->connected) {
		/* We still expect a response from the auth server. */
		return FALSE;
	}
	hash_table_remove(conn->requests, POINTER_CAST(request->id));
	request->removed = TRUE;
	return TRUE;
}
