/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */

#include "login-common.h"
#include "base64.h"
#include "buffer.h"
#include "hex-binary.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "safe-memset.h"
#include "str.h"
#include "str-sanitize.h"
#include "uri-util.h"
#include "auth-client.h"
#include "pop3-capability.h"
#include "pop3-protocol.h"
#include "client.h"
#include "client-authenticate.h"
#include "pop3-proxy.h"


static const char *capability_string = POP3_CAPABILITY_REPLY;

bool cmd_capa(struct pop3_client *client, const char *args ATTR_UNUSED)
{
	const struct auth_mech_desc *mech;
	unsigned int i, count;
	string_t *str;

	str = t_str_new(128);
	str_append(str, "+OK\r\n");
	str_append(str, capability_string);

	if (client_is_tls_enabled(&client->common) &&
	    !client->common.connection_tls_secured &&
	    !client->common.haproxy_terminated_tls)
		str_append(str, "STLS\r\n");
	if (client->common.set->auth_allow_cleartext ||
	    client->common.connection_secured)
		str_append(str, "USER\r\n");

	str_append(str, "SASL");
	mech = sasl_server_get_advertised_mechs(&client->common, &count);
	for (i = 0; i < count; i++) {
		str_append_c(str, ' ');
		str_append(str, mech[i].name);
	}
	str_append(str, "\r\n.\r\n");

	client_send_raw(&client->common, str_c(str));
	return TRUE;
}

static void
pop3_client_send_referral(struct client *client,
			  const struct client_auth_reply *reply)
{
	string_t *line = t_str_new(128);

	str_append(line, "-ERR [REFERRAL/");

	struct uri_host host = {
		.name = reply->proxy.host,
		.ip = reply->proxy.host_ip,
	};
	if (reply->proxy.username != NULL)
		uri_append_userinfo(line, reply->proxy.username);
	uri_append_host(line, &host);
	if (reply->proxy.port != POP3_DEFAULT_PORT)
		uri_append_port(line, reply->proxy.port);

	str_append(line, "] Referral\r\n");

	client_send_raw_data(client, str_data(line), str_len(line));
}

void pop3_client_auth_result(struct client *client,
			     enum client_auth_result result,
			     const struct client_auth_reply *reply,
			     const char *text)
{
	switch (result) {
	case CLIENT_AUTH_RESULT_SUCCESS:
		/* nothing to be done for POP3 */
		break;
	case CLIENT_AUTH_RESULT_REFERRAL_NOLOGIN:
		pop3_client_send_referral(client, reply);
		break;
	case CLIENT_AUTH_RESULT_TEMPFAIL:
		client_send_reply(client, POP3_CMD_REPLY_TEMPFAIL, text);
		break;
	case CLIENT_AUTH_RESULT_LIMIT_REACHED:
		client_send_reply(client, POP3_CMD_REPLY_LIMIT, text);
		break;
	case CLIENT_AUTH_RESULT_AUTHFAILED:
	case CLIENT_AUTH_RESULT_AUTHFAILED_REASON:
	case CLIENT_AUTH_RESULT_AUTHZFAILED:
	case CLIENT_AUTH_RESULT_PASS_EXPIRED:
	case CLIENT_AUTH_RESULT_SSL_REQUIRED:
	case CLIENT_AUTH_RESULT_LOGIN_DISABLED:
	case CLIENT_AUTH_RESULT_MECH_INVALID:
	case CLIENT_AUTH_RESULT_MECH_SSL_REQUIRED:
	case CLIENT_AUTH_RESULT_INVALID_BASE64:
		client_send_reply(client, POP3_CMD_REPLY_AUTH_ERROR, text);
		break;
	default:
		client_send_reply(client, POP3_CMD_REPLY_ERROR, text);
		break;
	}
}

int cmd_auth(struct pop3_client *pop3_client)
{
	/* NOTE: This command's input is handled specially because the
	   SASL-IR can be large. */
	struct client *client = &pop3_client->common;
	const unsigned char *data;
	size_t i, size;
	int ret;

	/* <auth mechanism name> [<initial SASL response>] */
	if (!pop3_client->auth_mech_name_parsed) {
		data = i_stream_get_data(client->input, &size);
		for (i = 0; i < size; i++) {
			if (data[i] == ' ' ||
			    data[i] == '\r' || data[i] == '\n')
				break;
		}
		if (i == size)
			return 0;
		if (i == 0) {
			/* Old-style SASL discovery, used by MS Outlook */
			unsigned int i, count;
			const struct auth_mech_desc *mech;

			client_send_raw(client, "+OK\r\n");
			mech = sasl_server_get_advertised_mechs(client, &count);
			for (i = 0; i < count; i++) {
				client_send_raw(client, mech[i].name);
				client_send_raw(client, "\r\n");
			}
			client_send_raw(client, ".\r\n");
			(void)i_stream_read_next_line(client->input);
			return 1;
		}
		i_free(client->auth_mech_name);
		client->auth_mech_name = i_strndup(data, i);
		pop3_client->auth_mech_name_parsed = TRUE;
		if (data[i] == ' ')
			i++;
		i_stream_skip(client->input, i);
	}

	/* get SASL-IR, if any */
	if ((ret = client_auth_read_line(client)) <= 0)
		return ret;

	const char *ir = NULL;
	if (client->auth_response->used > 0)
		ir = t_strdup(str_c(client->auth_response));

	pop3_client->auth_mech_name_parsed = FALSE;
	/* The whole AUTH line command is parsed now. The rest of the SASL
	   protocol exchange happens in login-common code. We can free the
	   current command here already, because no pop3-login code is called
	   until the authentication is finished. Also, there's currently no
	   single location that is called in pop3-login code after the
	   authentication is finished. For example it could be an auth failure
	   or it could be a successful authentication with a proxying
	   failure. */
	i_free(pop3_client->current_cmd);
	return client_auth_begin(client, t_strdup(client->auth_mech_name), ir);
}

bool cmd_user(struct pop3_client *pop3_client, const char *args)
{
	if (!client_check_plaintext_auth(&pop3_client->common, FALSE)) {
		if (pop3_client->common.virtual_user == NULL)
			pop3_client->common.virtual_user = i_strdup(args);
		return TRUE;
	}

	i_free(pop3_client->last_user);
	pop3_client->last_user = i_strdup(args);

	client_send_raw(&pop3_client->common, "+OK\r\n");
	return TRUE;
}

bool cmd_pass(struct pop3_client *pop3_client, const char *args)
{
	struct client *client = &pop3_client->common;
	string_t *plain_login, *base64;

	if (pop3_client->last_user == NULL) {
		/* client may ignore the USER reply and only display the error
		   message from PASS */
		if (!client_check_plaintext_auth(client, TRUE))
			return TRUE;

		client_send_reply(client, POP3_CMD_REPLY_ERROR,
				  "No username given.");
		return TRUE;
	}

	/* authorization ID \0 authentication ID \0 pass */
	plain_login = t_str_new(128);
	str_append_c(plain_login, '\0');
	str_append(plain_login, pop3_client->last_user);
	str_append_c(plain_login, '\0');
	str_append(plain_login, args);

	i_free_and_null(pop3_client->last_user);

	base64 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(plain_login->used));
	base64_encode(plain_login->data, plain_login->used, base64);

	(void)client_auth_begin(client, SASL_MECH_NAME_PLAIN, str_c(base64));
	return TRUE;
}

bool cmd_apop(struct pop3_client *pop3_client, const char *args)
{
	struct client *client = &pop3_client->common;
	buffer_t *apop_data, *base64;
	const char *p;
	unsigned int server_pid, connect_uid;

	if (pop3_client->apop_challenge == NULL) {
		if (client->set->auth_verbose)
			e_info(client->event, "APOP failed: APOP not enabled");
		client_send_reply(client, POP3_CMD_REPLY_ERROR,
				  "APOP not enabled.");
		return TRUE;
	}

	/* <username> <md5 sum in hex> */
	p = strchr(args, ' ');
	if (p == NULL || strlen(p+1) != 32) {
		if (client->set->auth_verbose)
			e_info(client->event, "APOP failed: Invalid parameters");
		client_send_reply(client, POP3_CMD_REPLY_ERROR,
				  "Invalid parameters.");
		return TRUE;
	}

	/* APOP challenge \0 username \0 APOP response */
	apop_data = t_buffer_create(128);
	buffer_append(apop_data, pop3_client->apop_challenge,
		      strlen(pop3_client->apop_challenge)+1);
	buffer_append(apop_data, args, (size_t)(p-args));
	buffer_append_c(apop_data, '\0');

	if (hex_to_binary(p+1, apop_data) < 0) {
		if (client->set->auth_verbose) {
			e_info(client->event, "APOP failed: "
			       "Invalid characters in MD5 response");
		}
		client_send_reply(client, POP3_CMD_REPLY_ERROR,
				  "Invalid characters in MD5 response.");
		return TRUE;
	}

	base64 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(apop_data->used));
	base64_encode(apop_data->data, apop_data->used, base64);

	auth_client_get_connect_id(auth_client, &server_pid, &connect_uid);
	if (pop3_client->apop_server_pid != server_pid ||
	    pop3_client->apop_connect_uid != connect_uid) {
		/* we reconnected to auth server and can't authenticate
		   with APOP in this session anymore. disconnecting the user
		   is probably the best solution now. */
		client_destroy(client,
			"Reconnected to auth server, can't do APOP");
		return TRUE;
	}

	(void)client_auth_begin_private(client, AUTH_SASL_MECH_NAME_APOP,
					str_c(base64));
	return TRUE;
}
