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

#include "lib.h"
#include "net.h"
#include "ioloop.h"
#include "eacces-error.h"
#include "hostpid.h"
#include "istream.h"
#include "ostream.h"
#include "llist.h"
#include "hex-binary.h"
#include "hash.h"
#include "str.h"
#include "strescape.h"
#include "time-util.h"
#include "connection.h"
#include "auth-client-private.h"
#include "master-interface.h"
#include "master-service.h"
#include "login-interface.h"
#include "login-server-auth.h"


#define AUTH_MAX_INBUF_SIZE 8192

struct login_server_auth_request {
	struct login_server_auth_request *prev, *next;
	struct event *event;

	unsigned int id;
	struct timeval create_stamp;

	pid_t auth_pid;
	unsigned int auth_id;
	unsigned int client_pid;
	uint8_t cookie[LOGIN_REQUEST_COOKIE_SIZE];

	login_server_auth_request_callback_t *callback;
	void *context;

	bool aborted:1;
};

struct login_server_auth {
	struct connection conn;
	struct connection_list *clist;
	struct event *event;
	pool_t pool;
	int refcount;

	const char *auth_socket_path;

	struct timeval connect_time, handshake_time;

	struct timeout *to;

	unsigned int id_counter;
	HASH_TABLE(void *, struct login_server_auth_request *) requests;
	/* linked list of requests, ordered by create_stamp */
	struct login_server_auth_request *request_head, *request_tail;

	pid_t auth_server_pid;

	unsigned int timeout_msecs;

	bool connected:1;
	bool request_auth_token:1;
};

static int
login_server_auth_input_args(struct connection *_conn, const char *const *args);
static int
login_server_auth_handshake_line(struct connection *_conn, const char *line);
static void login_server_auth_destroy(struct connection *_conn);

static void login_server_auth_update_timeout(struct login_server_auth *auth);
static void login_server_auth_check_spids(struct login_server_auth *auth);

static const struct connection_vfuncs login_server_auth_vfuncs = {
	.destroy = login_server_auth_destroy,
	.handshake_line = login_server_auth_handshake_line,
	.input_args = login_server_auth_input_args,
};

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

static int
login_server_auth_connect(struct login_server_auth *auth);

struct login_server_auth *
login_server_auth_init(const char *auth_socket_path, bool request_auth_token)
{
	struct login_server_auth *auth;
	pool_t pool;

	pool = pool_alloconly_create("login server auth", 1024);
	auth = p_new(pool, struct login_server_auth, 1);
	auth->pool = pool;
	auth->auth_socket_path = p_strdup(pool, auth_socket_path);
	auth->request_auth_token = request_auth_token;
	auth->refcount = 1;
	hash_table_create_direct(&auth->requests, pool, 0);
	auth->id_counter = i_rand_limit(32767) * 131072U;

	auth->clist = connection_list_init(&login_server_auth_set,
					   &login_server_auth_vfuncs);

	auth->event = event_create(NULL);
	event_add_category(auth->event, &event_category_auth_client);
	event_set_append_log_prefix(auth->event, "auth-master: login: ");

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

	auth->timeout_msecs = 1000 * MASTER_AUTH_LOOKUP_TIMEOUT_SECS;
	login_server_auth_connect(auth);
	return auth;
}

static void request_failure(struct login_server_auth *auth,
			    struct login_server_auth_request *request,
			    const char *log_reason, const char *client_reason)
{
	string_t *str = t_str_new(128);

	str_printfa(str, "auth connected %lld msecs ago",
		    timeval_diff_msecs(&ioloop_timeval, &auth->connect_time));
	if (auth->handshake_time.tv_sec != 0) {
		str_printfa(str, ", handshake %lld msecs ago",
			    timeval_diff_msecs(&ioloop_timeval, &auth->handshake_time));
	}
	str_printfa(str, ", request took %lld msecs, client-pid=%u client-id=%u",
		    timeval_diff_msecs(&ioloop_timeval, &request->create_stamp),
		    request->client_pid, request->auth_id);

	struct event_passthrough *e =
		event_create_passthrough(request->event)->
		set_name("auth_master_client_login_finished");
	e->add_str("error", log_reason);
	e_error(e->event(), "Login auth request failed: %s (%s)",
		log_reason, str_c(str));

	request->callback(NULL, client_reason, request->context);
}

static void
request_internal_failure(struct login_server_auth *auth,
			 struct login_server_auth_request *request,
			 const char *reason)
{
	request_failure(auth, request, reason, LOGIN_REQUEST_ERRMSG_INTERNAL_FAILURE);
}

static void request_free(struct login_server_auth_request **_request)
{
	struct login_server_auth_request *request = *_request;

	*_request = NULL;

	event_unref(&request->event);
	i_free(request);
}

static void
login_server_auth_fail(struct login_server_auth *auth,
		       const char *reason) ATTR_NULL(2)
{
	struct login_server_auth_request *request;

	if (reason == NULL)
		reason = "Disconnected from auth server, aborting";

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

	while (auth->request_head != NULL) {
		request = auth->request_head;
		DLLIST2_REMOVE(&auth->request_head,
			       &auth->request_tail, request);

		request_internal_failure(auth, request, reason);
		request_free(&request);
	}
	hash_table_clear(auth->requests, FALSE);

	timeout_remove(&auth->to);
	i_zero(&auth->connect_time);
	i_zero(&auth->handshake_time);
}

void login_server_auth_disconnect(struct login_server_auth *auth)
{
	login_server_auth_fail(auth, NULL);
}

static void login_server_auth_unref(struct login_server_auth **_auth)
{
	struct login_server_auth *auth = *_auth;
	struct connection_list *clist = auth->clist;

	*_auth = NULL;

	i_assert(auth->refcount > 0);
	if (--auth->refcount > 0)
		return;

	hash_table_destroy(&auth->requests);
	connection_deinit(&auth->conn);
	connection_list_deinit(&clist);
	event_unref(&auth->event);
	pool_unref(&auth->pool);
}

void login_server_auth_deinit(struct login_server_auth **_auth)
{
	struct login_server_auth *auth = *_auth;

	*_auth = NULL;

	login_server_auth_disconnect(auth);
	login_server_auth_unref(&auth);
}

void login_server_auth_set_timeout(struct login_server_auth *auth,
				   unsigned int msecs)
{
	auth->timeout_msecs = msecs;
}

static void login_server_auth_destroy(struct connection *_conn)
{
	struct login_server_auth *auth =
		container_of(_conn, struct login_server_auth, conn);

	switch (_conn->disconnect_reason) {
	case CONNECTION_DISCONNECT_HANDSHAKE_FAILED:
		login_server_auth_fail(auth,
				       "Handshake with auth service failed");
		break;
	case CONNECTION_DISCONNECT_BUFFER_FULL:
		/* buffer full */
		e_error(auth->event, "Auth server sent us too long line");
		login_server_auth_fail(auth, NULL);
		break;
	default:
		/* disconnected. stop accepting new connections, because in
		   default configuration we no longer have permissions to
		   connect back to auth-master */
		master_service_stop_new_connections(master_service);
		login_server_auth_fail(auth, NULL);
	}
}

static unsigned int auth_get_next_timeout_msecs(struct login_server_auth *auth)
{
	struct timeval expires;
	long long diff;

	expires = auth->request_head->create_stamp;
	timeval_add_msecs(&expires, auth->timeout_msecs);

	diff = timeval_diff_msecs(&expires, &ioloop_timeval);
	return (diff <= 0 ? 0 : (unsigned int)diff);
}

static void login_server_auth_timeout(struct login_server_auth *auth)
{
	struct login_server_auth_request *request;
	const char *reason;

	while (auth->request_head != NULL &&
	       auth_get_next_timeout_msecs(auth) == 0) {
		long long msecs;

		request = auth->request_head;
		DLLIST2_REMOVE(&auth->request_head,
			       &auth->request_tail, request);
		hash_table_remove(auth->requests, POINTER_CAST(request->id));

		msecs = timeval_diff_msecs(&ioloop_timeval,
					   &request->create_stamp);
		reason = t_strdup_printf(
			"Auth server request timed out after %lld.%03lld secs",
			msecs/1000, msecs%1000);
		request_internal_failure(auth, request, reason);
		request_free(&request);
	}
	timeout_remove(&auth->to);
	login_server_auth_update_timeout(auth);
}

static void login_server_auth_update_timeout(struct login_server_auth *auth)
{
	i_assert(auth->to == NULL);

	if (auth->request_head != NULL) {
		auth->to = timeout_add(auth_get_next_timeout_msecs(auth),
				       login_server_auth_timeout, auth);
	}
}

static int
login_server_auth_handshake_line(struct connection *_conn, const char *line)
{
	struct login_server_auth *auth =
		container_of(_conn, struct login_server_auth, conn);
	const char *const *tmp;
	unsigned int major_version, minor_version;

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

		if (connection_verify_version(_conn, "auth-master",
					      major_version,
					      minor_version) < 0)
			return -1;
		return 0;
	}
	if (strcmp(tmp[0], "SPID") != 0 ||
	    str_to_pid(tmp[1], &auth->auth_server_pid) < 0) {
		e_error(auth->event,
			"Auth server did not send valid SPID: %s", line);
		return -1;
	}

	login_server_auth_check_spids(auth);
	return 1;
}

static void
login_server_auth_request_remove(struct login_server_auth *auth,
				 struct login_server_auth_request *request)
{
	bool update_timeout;

	update_timeout = request->prev == NULL;

	hash_table_remove(auth->requests, POINTER_CAST(request->id));
	DLLIST2_REMOVE(&auth->request_head, &auth->request_tail, request);

	if (update_timeout) {
		timeout_remove(&auth->to);
		login_server_auth_update_timeout(auth);
	}
}

static struct login_server_auth_request *
login_server_auth_lookup_request(struct login_server_auth *auth,
				 unsigned int id)
{
	struct login_server_auth_request *request;

	request = hash_table_lookup(auth->requests, POINTER_CAST(id));
	if (request == NULL) {
		e_error(auth->event,
			"Auth server sent reply with unknown ID %u", id);
		return NULL;
	}
	login_server_auth_request_remove(auth, request);
	if (request->aborted) {
		request->callback(NULL, LOGIN_REQUEST_ERRMSG_INTERNAL_FAILURE,
				  request->context);
		request_free(&request);
		return NULL;
	}
	return request;
}

static void
login_server_auth_input_user(struct login_server_auth *auth, unsigned int id,
			     const char *const *args)
{
	struct login_server_auth_request *request;

	/* USER <id> <userid> [..] */
	request = login_server_auth_lookup_request(auth, id);
	if (request != NULL) {
		struct event_passthrough *e =
			event_create_passthrough(request->event)->
			set_name("auth_master_client_login_finished");
		if (args[0] != NULL && *args[0] != '\0')
			e->add_str("user", args[0]);
		e_debug(e->event(), "Login auth request successful");

		request->callback(args, NULL, request->context);
		request_free(&request);
	}
}

static void
login_server_auth_input_notfound(struct login_server_auth *auth,
				 unsigned int id,
				 const char *const *args ATTR_UNUSED)
{
	struct login_server_auth_request *request;

	/* NOTFOUND <id> */
	request = login_server_auth_lookup_request(auth, id);
	if (request != NULL) {
		const char *reason = t_strdup_printf(
			"Authenticated user not found from userdb, "
			"auth lookup id=%u", id);
		request_internal_failure(auth, request, reason);
		request_free(&request);
	}
}

static void
login_server_auth_input_fail(struct login_server_auth *auth, unsigned int id,
			     const char *const *args)
{
	struct login_server_auth_request *request;
	const char *error = NULL;
	unsigned int i;

	/* FAIL <id> [..] [reason=<error>] [..] */
	for (i = 0; args[i] != NULL; i++) {
		if (str_begins(args[i], "reason=", &error))
			break;
	}

	request = login_server_auth_lookup_request(auth, id);
	if (request != NULL) {
		if (error == NULL) {
			request_internal_failure(auth, request,
						 "Internal auth failure");
		} else {
			const char *log_reason = t_strdup_printf(
				"Internal auth failure: %s", error);
			request_failure(auth, request, log_reason, error);
		}
		request_free(&request);
	}
}

static int
login_server_auth_input_args(struct connection *_conn, const char *const *args)
{
	struct login_server_auth *auth =
		container_of(_conn, struct login_server_auth, conn);
	unsigned int id;

	if (args[0] != NULL && strcmp(args[0], "CUID") == 0) {
		e_error(auth->event, "%s is an auth client socket. "
			"It should be a master socket.",
			auth->auth_socket_path);
		return -1;
	}

	if (args[0] == NULL || args[1] == NULL ||
	    str_to_uint(args[1], &id) < 0) {
		e_error(auth->event, "BUG: Unexpected input: %s",
			t_strarray_join(args, "\t"));
		return -1;
	}

	auth->refcount++;
	if (strcmp(args[0], "USER") == 0)
		login_server_auth_input_user(auth, id, &args[2]);
	else if (strcmp(args[0], "NOTFOUND") == 0)
		login_server_auth_input_notfound(auth, id, &args[2]);
	else if (strcmp(args[0], "FAIL") == 0)
		login_server_auth_input_fail(auth, id, &args[2]);
	login_server_auth_unref(&auth);

	return 1;
}

static int
login_server_auth_connect(struct login_server_auth *auth)
{
	i_assert(!auth->connected);

	if (connection_client_connect(&auth->conn) < 0) {
		if (errno == EACCES) {
			e_error(auth->event, "%s",
				eacces_error_get("connect",
						 auth->auth_socket_path));
		} else {
			e_error(auth->event, "connect(%s) failed: %m",
				auth->auth_socket_path);
		}
		return -1;
	}
	io_loop_time_refresh();
	auth->connect_time = ioloop_timeval;
	auth->connected = TRUE;

	o_stream_nsend_str(auth->conn.output,
		t_strdup_printf("VERSION\t%u\t%u\n",
				AUTH_CLIENT_PROTOCOL_MAJOR_VERSION,
				AUTH_CLIENT_PROTOCOL_MINOR_VERSION));
	return 0;
}

static bool
auth_request_check_spid(struct login_server_auth *auth,
			struct login_server_auth_request *req)
{
	if (auth->auth_server_pid != req->auth_pid &&
	    connection_handshake_received(&auth->conn)) {
		/* auth server was restarted. don't even attempt a login. */
		e_warning(auth->event,
			  "Auth server restarted (pid %u -> %u), aborting auth",
			  (unsigned int)req->auth_pid,
			  (unsigned int)auth->auth_server_pid);
		return FALSE;
	}
	return TRUE;
}

static void login_server_auth_check_spids(struct login_server_auth *auth)
{
	struct login_server_auth_request *req, *next;

	for (req = auth->request_head; req != NULL; req = next) {
		next = req->next;
		if (!auth_request_check_spid(auth, req))
			req->aborted = TRUE;
	}
}

static void
login_server_auth_send_request(struct login_server_auth *auth,
			       struct login_server_auth_request *req)
{
	string_t *str;

	if (!auth_request_check_spid(auth, req)) {
		login_server_auth_request_remove(auth, req);
		req->callback(NULL, LOGIN_REQUEST_ERRMSG_INTERNAL_FAILURE,
			      req->context);
		request_free(&req);
		return;
	}

	str = t_str_new(128);
	str_printfa(str, "REQUEST\t%u\t%u\t%u\t", req->id,
		    req->client_pid, req->auth_id);
	binary_to_hex_append(str, req->cookie, sizeof(req->cookie));
	str_printfa(str, "\tsession_pid=%s", my_pid);
	if (auth->request_auth_token)
		str_append(str, "\trequest_auth_token");
	str_append_c(str, '\n');
	o_stream_nsend(auth->conn.output, str_data(str), str_len(str));
}

void login_server_auth_request(struct login_server_auth *auth,
			       const struct login_request *req,
			       login_server_auth_request_callback_t *callback,
			       void *context)
{
	struct login_server_auth_request *login_req;
	unsigned int id;

	if (!auth->connected) {
		if (login_server_auth_connect(auth) < 0) {
			/* we couldn't connect to auth now,
			   so we probably can't in future either. */
			master_service_stop_new_connections(master_service);
			callback(NULL, LOGIN_REQUEST_ERRMSG_INTERNAL_FAILURE,
				 context);
			return;
		}
	}

	id = ++auth->id_counter;
	if (id == 0)
		id++;

	io_loop_time_refresh();
	login_req = i_new(struct login_server_auth_request, 1);
	login_req->create_stamp = ioloop_timeval;
	login_req->id = id;
	login_req->auth_pid = req->auth_pid;
	login_req->client_pid = req->client_pid;
	login_req->auth_id = req->auth_id;
	memcpy(login_req->cookie, req->cookie, sizeof(login_req->cookie));
	login_req->callback = callback;
	login_req->context = context;
	i_assert(hash_table_lookup(auth->requests, POINTER_CAST(id)) == NULL);
	hash_table_insert(auth->requests, POINTER_CAST(id), login_req);
	DLLIST2_APPEND(&auth->request_head, &auth->request_tail, login_req);

	login_req->event = event_create(auth->event);
	event_add_int(login_req->event, "id", login_req->id);
	event_set_append_log_prefix(login_req->event,
				    t_strdup_printf("request [%u]: ",
						    login_req->id));

	if (req->local_ip.family != 0)
		event_add_ip(login_req->event, "local_ip", &req->local_ip);
	if (req->local_port != 0) {
		event_add_int(login_req->event, "local_port",
			      req->local_port);
	}
	if (req->remote_ip.family != 0)
		event_add_ip(login_req->event, "remote_ip", &req->remote_ip);
	if (req->remote_port != 0) {
		event_add_int(login_req->event, "remote_port",
			      req->remote_port);
	}

	struct event_passthrough *e =
		event_create_passthrough(login_req->event)->
		set_name("auth_master_client_login_started");
	e_debug(e->event(), "Started login auth request");

	if (auth->to == NULL)
		login_server_auth_update_timeout(auth);

	login_server_auth_send_request(auth, login_req);
}

unsigned int login_server_auth_request_count(struct login_server_auth *auth)
{
	return hash_table_count(auth->requests);
}
