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

#include "login-common.h"
#include "str.h"
#include "strescape.h"
#include "base64.h"
#include "net.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "version.h"
#include "auth-client.h"
#include "client-common.h"

#define IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION 2
#define IMAP_URLAUTH_PROTOCOL_MINOR_VERSION 0

struct imap_urlauth_client {
	struct client common;

	bool version_received:1;
};

static void
imap_urlauth_client_auth_result(struct client *client,
				enum client_auth_result result,
				const struct client_auth_reply *reply ATTR_UNUSED,
				const char *text ATTR_UNUSED)
{
	if (result != CLIENT_AUTH_RESULT_SUCCESS) {
		/* failed or otherwise invalid status */
		client_send_raw(client, "FAILED\n");
		client_destroy(client, "Authentication failed");
	} else {
		/* authentication succeeded */
	}
}

static void imap_urlauth_client_handle_input(struct client *client)
{
#define AUTH_ARG_COUNT 6
	struct imap_urlauth_client *uauth_client =
		(struct imap_urlauth_client *)client;
	struct net_unix_cred cred;
	const char *line;
	const char *const *args;
	pid_t pid;

	if (!uauth_client->version_received) {
		if ((line = i_stream_next_line(client->input)) == NULL)
			return;

		if (!version_string_verify(line, "imap-urlauth",
				IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION)) {
			e_error(client->event,
				"IMAP URLAUTH client not compatible with this server "
				"(mixed old and new binaries?) %s", line);
			client_destroy(client, "Version mismatch");
			return;
		}
		uauth_client->version_received = TRUE;
	}

	if ((line = i_stream_next_line(client->input)) == NULL)
		return;

	/* read authentication info from input;
	   "AUTH"\t<service>\t<session-pid>\t<auth-username>\t<session_id>\t<token> */
	args = t_strsplit_tabescaped(line);
	if (str_array_length(args) < AUTH_ARG_COUNT ||
	    strcmp(args[0], "AUTH") != 0 || str_to_pid(args[2], &pid) < 0) {
		e_error(client->event,
			"IMAP URLAUTH client sent unexpected AUTH input: %s", line);
		client_destroy(client, "Unexpected input");
		return;
	}

	/* only imap and submission have direct access to urlauth service */
	if (strcmp(args[1], "imap") != 0 && strcmp(args[1], "submission") != 0) {
		e_error(client->event,
			"IMAP URLAUTH accessed from inappropriate service: %s", args[1]);
		client_destroy(client, "Unexpected input");
		return;
	}

	/* verify session pid if possible */
	if (net_getunixcred(client->fd, &cred) == 0 &&
	    cred.pid != (pid_t)-1 && pid != cred.pid) {
		e_error(client->event,
			"IMAP URLAUTH client sent invalid session pid %ld in AUTH request: "
			"it did not match peer credentials (pid=%ld, uid=%ld)",
			(long)pid, (long)cred.pid, (long)cred.uid);
		client_destroy(client, "Invalid AUTH request");
		return;
	}

	T_BEGIN {
		string_t *auth_data = t_str_new(128);
		string_t *init_resp;
		unsigned int i;

		str_append(auth_data, args[1]);
		for (i = 2; i < AUTH_ARG_COUNT; i++) {
			str_append_c(auth_data, '\0');
			str_append(auth_data, args[i]);
		}
		init_resp = t_str_new(256);
		base64_encode(str_data(auth_data),
			      str_len(auth_data), init_resp);

		(void)client_auth_begin_private(client,
			AUTH_SASL_MECH_NAME_DOVECOT_TOKEN, str_c(init_resp));
	} T_END;
}

static void imap_urlauth_client_input(struct client *client)
{
	if (!client_read(client))
		return;

	client_ref(client);
	o_stream_cork(client->output);
	if (!auth_client_is_connected(auth_client)) {
		/* we're not currently connected to auth process -
		   don't allow any commands */
		timeout_remove(&client->to_auth_waiting);
		client->input_blocked = TRUE;
	} else {
		imap_urlauth_client_handle_input(client);
	}
	o_stream_uncork(client->output);
	client_unref(&client);
}

static struct client *imap_urlauth_client_alloc(pool_t pool)
{
	struct imap_urlauth_client *uauth_client;

	uauth_client = p_new(pool, struct imap_urlauth_client, 1);
	return &uauth_client->common;
}

static int imap_urlauth_client_create(struct client *client ATTR_UNUSED)
{
	return 0;
}

static void imap_urlauth_client_notify_auth_ready(struct client *client)
{
	string_t *version;

	version = t_str_new(128);
	str_append(version, "VERSION\timap-urlauth");
	str_printfa(version, "\t%u", IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION);
	str_printfa(version, "\t%u", IMAP_URLAUTH_PROTOCOL_MINOR_VERSION);
	str_append(version, "\n");

	client_send_raw(client, str_c(version));

	client->banner_sent = TRUE;
	i_assert(client->io == NULL);
	client->io = io_add_istream(client->input, client_input, client);
}

static void imap_urlauth_login_preinit(void)
{
}

static void imap_urlauth_login_init(void)
{
}

static void imap_urlauth_login_deinit(void)
{
	clients_destroy_all();
}

static struct client_vfuncs imap_urlauth_vfuncs = {
	.alloc = imap_urlauth_client_alloc,
	.create = imap_urlauth_client_create,
	.notify_auth_ready = imap_urlauth_client_notify_auth_ready,
	.input = imap_urlauth_client_input,
	.auth_result = imap_urlauth_client_auth_result,
	.send_raw_data = client_common_send_raw_data,
	.free = client_common_default_free,
};

static struct login_binary imap_urlauth_login_binary = {
	.protocol = "imap-urlauth",
	.service_name = "imap-urlauth",
	.process_name = "imap-urlauth-login",
	.default_login_socket = LOGIN_TOKEN_DEFAULT_SOCKET,

	.event_category = {
		.name = "imap",
	},

	.client_vfuncs = &imap_urlauth_vfuncs,
	.preinit = imap_urlauth_login_preinit,
	.init = imap_urlauth_login_init,
	.deinit = imap_urlauth_login_deinit,

	.anonymous_login_acceptable = TRUE,

	.application_protocols = (const char *const[]) {
		"imap", NULL
	},
};

int main(int argc, char *argv[])
{
	return login_binary_run(&imap_urlauth_login_binary, argc, argv);
}
