/*
 *  Copyright (C) 2002 - 2003 Ardis Technologies <roman@ardistech.com>
 *  Copyright (C) 2007 - 2018 Vladislav Bolkhovitin
 *  Copyright (C) 2007 - 2018 Western Digital Corporation
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation, version 2
 *  of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 */

#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <unistd.h>

#include "iscsid.h"

struct __qelem targets_list = LIST_HEAD_INIT(targets_list);

int send_targets_link_local = DEFAULT_SEND_TARGETS_LINK_LOCAL;

const char *iscsi_make_full_initiator_name(int per_portal_acl,
	const char *initiator_name, const char *target_portal,
	char *buf, int size)
{
	if (per_portal_acl)
		snprintf(buf, size, "%s#%s", initiator_name,
			target_portal);
	else
		snprintf(buf, size, "%s", initiator_name);

	return buf;
}

/*
 * Written by Jack Handy - jakkhandy@hotmail.com
 * Taken by Gennadiy Nerubayev <parakie@gmail.com> from
 * http://www.codeproject.com/KB/string/wildcmp.aspx. No license attached
 * to it, and it's posted on a free site; assumed to be free for use.
 *
 * Added the negative sign support - VLNB
 *
 * Also see comment for wildcmp().
 *
 * SCST core also has a copy of this code, so fixing a bug here, don't forget
 * to fix the copy too!
 */
static int __wildcmp(const char *wild, const char *string, int recursion_level)
{
	const char *cp = NULL, *mp = NULL;

	while ((*string) && (*wild != '*')) {
		if ((*wild == '!') && (recursion_level == 0))
			return !__wildcmp(++wild, string, ++recursion_level);

		if ((tolower(*wild) != tolower(*string)) && (*wild != '?'))
			return 0;

		wild++;
		string++;
	}

	while (*string) {
		if ((*wild == '!') && (recursion_level == 0))
			return !__wildcmp(++wild, string, ++recursion_level);

		if (*wild == '*') {
			if (!*++wild)
				return 1;

			mp = wild;
			cp = string+1;
		} else if ((tolower(*wild) == tolower(*string)) || (*wild == '?')) {
			wild++;
			string++;
		} else {
			wild = mp;
			string = cp++;
		}
	}

	while (*wild == '*')
		wild++;

	return !*wild;
}

/*
 * Returns true if string "string" matches pattern "wild", false otherwise.
 * Pattern is a regular DOS-type pattern, containing '*' and '?' symbols.
 * '*' means match all any symbols, '?' means match only any single symbol.
 *
 * For instance:
 * if (wildcmp("bl?h.*", "blah.jpg")) {
 *   // match
 *  } else {
 *   // no match
 *  }
 *
 * Also it supports boolean inversion sign '!', which does boolean inversion of
 * the value of the rest of the string. Only one '!' allowed in the pattern,
 * other '!' are treated as regular symbols. For instance:
 * if (wildcmp("bl!?h.*", "blah.jpg")) {
 *   // no match
 *  } else {
 *   // match
 *  }
 *
 * Also see comment for __wildcmp().
 */
static int wildcmp(const char *wild, const char *string)
{
	return __wildcmp(wild, string, 0);
}

/*
 * Evaluate the list with wildcard patterns as follows:
 * - If only positive wildcard patterns have been specified, it is sufficient
 *   that one wildcard pattern matches (logical or).
 * - If only negative wildcard patterns have been specified, none of these
 *   patterns must match (logical and).
 * - If positive and negative wildcard patterns have been specified, one of
 *   the positive patterns must match and none of the negative.
 */
int target_portal_allowed(struct target *target,
	const char *target_portal, const char *initiator_name)
{
	int res;
	char full_initiator_name[ISCSI_FULL_NAME_LEN];

	if (!list_empty(&target->allowed_portals)) {
		struct iscsi_attr *attr;
		bool any_pos_cond = false, pos_match = false, neg_match = true;

		list_for_each_entry(attr, &target->allowed_portals, ulist) {
			bool match = wildcmp(attr->attr_key, target_portal);

			if (attr->attr_key[0] != '!') {
				any_pos_cond = true;
				pos_match = pos_match || match;
			} else {
				neg_match = neg_match && match;
			}
		}
		res = (!any_pos_cond || pos_match) && neg_match;
		if (res == 0)
			goto out;
	}

	res = kernel_initiator_allowed(target->tid,
		iscsi_make_full_initiator_name(target->per_portal_acl,
			initiator_name, target_portal,
			full_initiator_name, sizeof(full_initiator_name)));
	if (res < 0)
		res = 0; /* false */

out:
	return res;
}

static int is_addr_loopback(char *addr)
{
	struct in_addr ia;
	struct in6_addr ia6;

	if (inet_pton(AF_INET, addr, &ia) == 1)
		return !strncmp(addr, "127.", 4);

	if (inet_pton(AF_INET6, addr, &ia6) == 1)
		return IN6_IS_ADDR_LOOPBACK(&ia6);

	return 0;
}

static int is_addr_unspecified(char *addr)
{
	struct in_addr ia;
	struct in6_addr ia6;

	if (inet_pton(AF_INET, addr, &ia) == 1)
		return (ia.s_addr == 0);

	if (inet_pton(AF_INET6, addr, &ia6) == 1)
		return IN6_IS_ADDR_UNSPECIFIED(&ia6);

	return 0;
}

static void target_print_addr(struct connection *conn, char *addr, int family)
{
	char taddr[NI_MAXHOST + NI_MAXSERV + 5];

	/* Maybe skip IPv6 link local addresses */
	if (family == AF_INET6 && !send_targets_link_local && (strncasecmp(addr, "fe80:", 5) == 0))
		return;

	snprintf(taddr, sizeof(taddr),
		(family == AF_INET) ? "%s:%d,1" : "[%s]:%d,1",
							addr, server_port);

	text_key_add(conn, "TargetAddress", taddr);
}

static void target_list_build_ifaddrs(struct connection *conn,
	struct target *target, char *exclude_addr, int family)
{
	struct ifaddrs *ifaddr, *ifa;
	char if_addr[NI_MAXHOST];

	getifaddrs(&ifaddr);

	for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
		if (!ifa->ifa_addr)
			continue;

		int sa_family = ifa->ifa_addr->sa_family;

		if (sa_family == family) {
			if (getnameinfo(ifa->ifa_addr, (family == AF_INET) ?
						sizeof(struct sockaddr_in) :
						sizeof(struct sockaddr_in6),
					if_addr, sizeof(if_addr),
					NULL, 0, NI_NUMERICHOST))
				continue;

			if (strcmp(exclude_addr, if_addr) &&
			    !is_addr_loopback(if_addr) &&
			    target_portal_allowed(target, if_addr, conn->initiator))
				target_print_addr(conn, if_addr, family);
		}
	}

	freeifaddrs(ifaddr);
	return;
}

void target_list_build(struct connection *conn, char *target_name)
{
	struct target *target;
	struct sockaddr_storage ss1, ss2;
	socklen_t slen = sizeof(struct sockaddr_storage);
	char portal[NI_MAXHOST];
	int family, i;

	if (conn->getsockname(conn->fd, (struct sockaddr *) &ss1, &slen)) {
		log_error("getsockname failed: %m");
		return;
	}

	family = ss1.ss_family;

	list_for_each_entry(target, &targets_list, tlist) {
		if (target_name && strcmp(target->name, target_name))
			continue;

		if (!target->tgt_enabled ||
		    !isns_scn_access_allowed(target->tid, conn->initiator) ||
		    !config_initiator_access_allowed(target->tid, conn->fd) ||
		    !target_portal_allowed(target, conn->target_portal, conn->initiator))
			continue;

		text_key_add(conn, "TargetName", target->name);
		target_print_addr(conn, conn->target_portal, family);

		for (i = 0; i < LISTEN_MAX && poll_array[i].fd; i++) {
			slen = sizeof(struct sockaddr_storage);

			if (getsockname(poll_array[i].fd,
					(struct sockaddr *) &ss2, &slen))
				continue;

			if (getnameinfo((struct sockaddr *) &ss2, slen, portal,
					sizeof(portal), NULL, 0, NI_NUMERICHOST))
				continue;

			if (ss2.ss_family != family)
				continue;

			if (is_addr_unspecified(portal))
				target_list_build_ifaddrs(conn, target,
					conn->target_portal, family);
			else if (strcmp(conn->target_portal, portal) &&
				 !is_addr_loopback(portal) &&
				 target_portal_allowed(target, portal,
						       conn->initiator))
					target_print_addr(conn, portal, family);
		}
	}
	return;
}

u32 target_find_id_by_name(const char *name)
{
	struct target *target;

	list_for_each_entry(target, &targets_list, tlist) {
		if (!strcasecmp(target->name, name))
			return target->tid;
	}

	return 0;
}

struct target *target_find_by_name(const char *name)
{
	struct target *target;

	list_for_each_entry(target, &targets_list, tlist) {
		if (!strcasecmp(target->name, name))
			return target;
	}

	return NULL;
}

struct target *target_find_by_id(u32 tid)
{
	struct target *target;

	list_for_each_entry(target, &targets_list, tlist) {
		if (target->tid == tid)
			return target;
	}

	return NULL;
}

int target_del(u32 tid, u32 cookie)
{
	struct target *target;
	int err;

	err = kernel_target_destroy(tid, cookie);

	if (err < 0 && err != -ENOENT)
		return err;

	target = target_find_by_id(tid);
	if (!err && !target)
		/* A leftover kernel object was cleaned up - don't complain. */
		return 0;

	if (!target)
		return -ENOENT;

	while (1) {
		/* We might need to handle session(s) removal event(s) from the kernel */
		while (handle_iscsi_events(nl_fd, false) == 0)
			;

		/* Someone else may have already freed the target object by now. */
		target = target_find_by_id(tid);
		if (!target) {
			log_info("%s: the target with tid = %u was already freed", __func__, tid);
			return 0;
		}

		if (list_empty(&target->sessions_list))
			break;

		/* We have not yet received session(s) removal event(s), so keep waiting */
		log_debug(1, "Target %d has sessions, keep waiting", tid);
		usleep(50000);
	}

	/*
	 * Remove target from the list after waiting for all sessions
	 * deleted, because we are looking for this target in list during
	 * each session delete.
	 */
	list_del(&target->tlist);

	if (target->tgt_enabled)
		isns_target_deregister(target->name);

	target_free(target);

	return 0;
}

void target_free(struct target *target)
{
	accounts_free(&target->target_in_accounts);
	accounts_free(&target->target_out_accounts);

	iscsi_attrs_free(&target->allowed_portals);

	free(target);
	return;
}

int target_create(const char *name, struct target **out_target)
{
	int res = 0;
	struct target *target;

	if (name == NULL) {
		res = EINVAL;
		goto out;
	}

	target = malloc(sizeof(*target));
	if (target == NULL) {
		res = -ENOMEM;
		goto out;
	}

	memset(target, 0, sizeof(*target));
	strlcpy(target->name, name, sizeof(target->name));

	params_set_defaults(target->target_params, target_keys);
	params_set_defaults(target->session_params, session_keys);

	INIT_LIST_HEAD(&target->tlist);
	INIT_LIST_HEAD(&target->sessions_list);
	INIT_LIST_HEAD(&target->target_in_accounts);
	INIT_LIST_HEAD(&target->target_out_accounts);
	INIT_LIST_HEAD(&target->allowed_portals);
	INIT_LIST_HEAD(&target->isns_head);

	*out_target = target;

out:
	return res;
}

int target_add(struct target *target, u32 *tid, u32 cookie)
{
	int err;

	if (target_find_by_name(target->name)) {
		log_error("duplicated target %s", target->name);
		err = -EEXIST;
		goto out;
	}

	err = kernel_target_create(target, tid, cookie);
	if (err != 0)
		goto out;

	list_add_tail(&target->tlist, &targets_list);


out:
	return err;
}

bool target_redirected(struct target *target, struct connection *conn)
{
	bool res = false, rc;
	union {
		struct sockaddr sa;
		struct sockaddr_in sin;
		struct sockaddr_in6 sin6;
	} sa;
	socklen_t slen = sizeof(sa);
	char tmp[NI_MAXHOST + 1];
	char addr[NI_MAXHOST + 3];
	char redirect[NI_MAXHOST + NI_MAXSERV + 4];
	char *p;

	if (strlen(target->redirect.addr) == 0)
		goto out;

	rc = getsockname(conn->fd, (struct sockaddr *)&sa.sa, &slen);
	if (rc != 0) {
		log_error("getsockname() failed: %s", strerror(errno));
		goto out;
	}

	rc = getnameinfo(&sa.sa, sizeof(sa), tmp, sizeof(tmp), NULL, 0, NI_NUMERICHOST);
	if (rc != 0) {
		log_error("getnameinfo() failed: %s", get_error_str(rc));
		goto out;
	}

	if ((p = strrchr(tmp, '%')))
		*p = '\0';

	if (sa.sa.sa_family == AF_INET6)
		snprintf(addr, sizeof(addr), "[%s]", tmp);
	else
		snprintf(addr, sizeof(addr), "%s", tmp);

	snprintf(redirect, sizeof(redirect), "%s:%d", target->redirect.addr,
		target->redirect.port);

	if (strcmp(target->redirect.addr, addr)) {
		text_key_add(conn, "TargetAddress", redirect);
		res = true;
	}

out:
	return res;
}
