// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Use is subject to license terms.
 *
 * Copyright (c) 2014, 2017, Intel Corporation.
 */
/*
 * This file is part of Lustre, http://www.lustre.org/
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "config.h"
#include <sys/param.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <dirent.h>
#include <poll.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#ifdef HAVE_NETDB_H
# include <netdb.h>
#endif
#ifdef _NEW_BUILD_
# include "lgss_utils.h"
#else
# include "err_util.h"
# include <gssapi/gssapi.h>
#endif
#include "lsupport.h"

const char * lustre_svc_name[] =
{
	[LUSTRE_GSS_SVC_MGS]    = "MGS",
	[LUSTRE_GSS_SVC_MDS]    = "MDS",
	[LUSTRE_GSS_SVC_OSS]    = "OSS",
};

/* exclusive startup */

static struct __sem_s {
	char           *name;
	key_t           sem_key;
	int             sem_id;
} sems[2] = {
	[GSSD_CLI] = { "client",  0x3a92d473, 0 },
	[GSSD_SVC] = { "server",  0x3b92d473, 0 },
};

void gssd_init_unique(int type)
{
	struct __sem_s *sem = &sems[type];
	struct sembuf   sembuf;

	assert(type == GSSD_CLI || type == GSSD_SVC);

again:
	sem->sem_id = semget(sem->sem_key, 1, IPC_CREAT | IPC_EXCL | 0700);
	if (sem->sem_id == -1) {
		if (errno != EEXIST) {
			printerr(LL_ERR, "Create sem: %s\n", strerror(errno));
			exit(-1);
		}

		/* already exist. Note there's still a small window racing
		 * with other processes, due to the stupid semaphore semantics.
		 */
		sem->sem_id = semget(sem->sem_key, 0, 0700);
		if (sem->sem_id == -1) {
			if (errno == ENOENT) {
				printerr(LL_ERR,
					 "another instance just exit, try again\n");
				goto again;
			}

			printerr(LL_ERR, "Obtain sem: %s\n", strerror(errno));
			exit(-1);
		}
	} else {
		int val = 1;

		if (semctl(sem->sem_id, 0, SETVAL, val) == -1) {
			printerr(LL_ERR, "Initialize sem: %s\n",
				 strerror(errno));
			exit(-1);
		}
	}

	sembuf.sem_num = 0;
	sembuf.sem_op = -1;
	sembuf.sem_flg = IPC_NOWAIT | SEM_UNDO;

	if (semop(sem->sem_id, &sembuf, 1) != 0) {
		if (errno == EAGAIN) {
			printerr(LL_ERR, "Another instance is running, exit\n");
			exit(0);
		}
		printerr(LL_ERR, "Grab sem: %s\n", strerror(errno));
		exit(0);
	}

	printerr(LL_INFO, "Successfully created %s global identity\n",
		 sem->name);
}

void gssd_exit_unique(int type)
{
	assert(type == GSSD_CLI || type == GSSD_SVC);

	/*
	 * do nothing. we can't remove the sem here, otherwise the race
	 * window would be much bigger. So it's sad we have to leave the
	 * sem in the system forever.
	 */
}

/****************************************
 * client side resolvation:             *
 *    lnd/netid/nid => hostname         *
 ****************************************/

char gethostname_ex[PATH_MAX] = GSSD_DEFAULT_GETHOSTNAME_EX;

typedef int lnd_nid2hostname_t(char *lnd, uint32_t net, uint32_t addr,
                               char *buf, int buflen);

int getcanonname(const char *host, char *buf, int buflen)
{
	struct addrinfo hints;
	struct addrinfo *ai = NULL;
	struct addrinfo *aip = NULL;
	int err = 0;
	int rc = 0;

	if (!host || host[0] == '\0') {
		printerr(LL_ERR,
			 "network address or hostname was not specified\n");
		return -1;
	}

	if (!buf) {
		printerr(LL_ERR,
			 "canonical name buffer was not defined\n");
		return -1;
	}

	if (buflen <= 0) {
		printerr(LL_ERR,
			 "invalid canonical name buffer length: %d\n", buflen);
		return -1;
	}

	memset(&hints, 0, sizeof(struct addrinfo));
	hints.ai_family = AF_INET;
	hints.ai_flags = AI_CANONNAME;

	err = getaddrinfo(host, NULL, &hints, &ai);
	if (err != 0) {
		printerr(LL_ERR,
			 "failed to get addrinfo for %s: %s\n",
			 host, gai_strerror(err));
		return -1;
	}

	for (aip = ai; aip; aip = aip->ai_next) {
		if (aip->ai_canonname && aip->ai_canonname[0] != '\0')
			break;
	}

	if (!aip) {
		printerr(LL_ERR, "failed to get canonical name of %s\n", host);
		rc = -1;
		goto out;
	}

	if (strlen(aip->ai_canonname) >= buflen) {
		printerr(LL_ERR, "canonical name is too long: %s\n",
			 aip->ai_canonname);
		rc = -1;
		goto out;
	}

	strncpy(buf, aip->ai_canonname, buflen);

out:
	if (ai != NULL)
		freeaddrinfo(ai);
	return rc;
}

static int getaddrcanonname(const uint32_t addr, char *buf, int buflen)
{
	struct sockaddr_in srcaddr;
	char ipstr[INET_ADDRSTRLEN] = "\0";
	int err = 0;
	int rc = -1;

	if (!buf) {
		printerr(LL_ERR,
			 "canonical name buffer was not defined\n");
		goto out;
	}

	if (buflen <= 0) {
		printerr(LL_ERR,
			 "invalid canonical name buffer length: %d\n", buflen);
		goto out;
	}

	memset(&srcaddr, 0, sizeof(srcaddr));
	srcaddr.sin_family = AF_INET;
	srcaddr.sin_addr.s_addr = (in_addr_t)addr;

	err = getnameinfo((struct sockaddr *)&srcaddr, sizeof(srcaddr),
			  buf, buflen, NULL, 0, NI_NAMEREQD);
	if (err != 0) {
		if (inet_ntop(srcaddr.sin_family, &srcaddr.sin_addr, ipstr,
			      INET_ADDRSTRLEN))
			printerr(LL_ERR, "failed to get name for %s: %s\n",
				 ipstr, gai_strerror(err));
		else
			printerr(LL_ERR, "failed to get name for 0x%x: %s\n",
				 addr, gai_strerror(err));
		goto out;
	}

	rc = 0;

out:
	return rc;
}

/* FIXME what about IPv6? */
static
int ipv4_nid2hostname(char *lnd, uint32_t net, uint32_t addr,
		      char *buf, int buflen)
{
	addr = htonl(addr);

	if (getaddrcanonname(addr, buf, buflen) != 0) {
		printerr(LL_ERR, "%s: failed to get canonical name of 0x%x\n",
			 lnd, addr);
		return -1;
	}

	printerr(LL_INFO, "%s: net 0x%x, addr 0x%x => %s\n",
		 lnd, net, addr, buf);
	return 0;
}

static
int lolnd_nid2hostname(char *lnd, uint32_t net, uint32_t addr,
		       char *buf, int buflen)
{
	struct utsname uts;

	if (addr) {
		printerr(LL_ERR, "%s: addr is 0x%x, we expect 0\n", lnd, addr);
		return -1;
	}

	if (uname(&uts)) {
		printerr(LL_ERR, "%s: failed obtain local machine name\n", lnd);
		return -1;
	}

	if (getcanonname(uts.nodename, buf, buflen) != 0) {
		printerr(LL_ERR, "%s: failed to obtain canonical name of %s\n",
			 lnd, uts.nodename);
		return -1;
	}

	printerr(LL_DEBUG, "%s: addr 0x%x => %s\n", lnd, addr, buf);
	return 0;
}

static int is_space(char c)
{
	return (c == ' ' || c == '\t' || c == '\n');
}

static
int external_nid2hostname(char *lnd, uint32_t net, uint32_t addr,
			  char *namebuf, int namebuflen)
{
	const int bufsize = PATH_MAX + 256;
	char buf[bufsize], *head, *tail;
	FILE *fghn;

	sprintf(buf, "%s %s 0x%x 0x%x", gethostname_ex, lnd, net, addr);
	printerr(LL_INFO, "cmd: %s\n", buf);

	fghn = popen(buf, "r");
	if (fghn == NULL) {
		printerr(LL_ERR, "failed to call %s\n", gethostname_ex);
		return -1;
	}

	head = fgets(buf, bufsize, fghn);
	if (head == NULL) {
		printerr(LL_ERR, "can't read from %s\n", gethostname_ex);
		pclose(fghn);
		return -1;
	}
	if (pclose(fghn) == -1)
		printerr(LL_WARN, "pclose failed, continue\n");

	/* trim head/tail space */
	while (is_space(*head))
		head++;

	tail = head + strlen(head);
	if (tail <= head) {
		printerr(LL_ERR, "no output from %s\n", gethostname_ex);
		return -1;
	}
	while (is_space(*(tail - 1)))
		tail--;
	if (tail <= head) {
		printerr(LL_ERR, "output are all space from %s\n",
			 gethostname_ex);
		return -1;
	}
	*tail = '\0';

	/* start with '@' means error msg */
	if (head[0] == '@') {
		printerr(LL_ERR, "error from %s: %s\n",
			 gethostname_ex, &head[1]);
		return -1;
	}

	if (tail - head > namebuflen) {
		printerr(LL_ERR, "external hostname too long: %s\n", head);
		return -1;
	}

	printerr(LL_INFO, "%s: net 0x%x, addr 0x%x => %s\n",
		 lnd, net, addr, head);
	strcpy(namebuf, head);
	return 0;
}

struct convert_struct {
	char                    *name;
	lnd_nid2hostname_t      *nid2name;
};

static struct convert_struct converter[] = {
	[0]	  = { .name = "UNUSED0" },
	[SOCKLND] = { .name = "SOCKLND", .nid2name = ipv4_nid2hostname },
	[O2IBLND] = { .name = "O2IBLND", .nid2name = ipv4_nid2hostname },
	[LOLND]	  = { .name = "LOLND",	 .nid2name = lolnd_nid2hostname },
	[PTL4LND] = { .name = "PTL4LND", .nid2name = external_nid2hostname },
	[KFILND]  = { .name = "KFILND",  .nid2name = ipv4_nid2hostname },
	[EFALND]  = { .name = "EFALND",  .nid2name = ipv4_nid2hostname },
};

#define LND_MAX         (sizeof(converter) / sizeof(converter[0]))

int lnet_nid2hostname(lnet_nid_t nid, char *buf, int buflen)
{
	uint32_t lnd, net, addr;

	addr = LNET_NIDADDR(nid);
	net = LNET_NIDNET(nid);
	lnd = LNET_NETTYP(net);

	if (lnd >= LND_MAX) {
		printerr(LL_ERR, "ERROR: Unrecognized LND %u\n", lnd);
		return -1;
	}

	if (converter[lnd].nid2name == NULL) {
		printerr(LL_ERR, "ERROR: %s converter not ready\n",
			converter[lnd].name);
		return -1;
	}

	return converter[lnd].nid2name(converter[lnd].name, net, addr,
				       buf, buflen);
}

uid_t parse_uid(char *uidstr)
{
	struct passwd *pw;
	char *p = NULL;
	long uid;

	pw = getpwnam(uidstr);
	if (pw)
		return pw->pw_uid;

	uid = strtol(uidstr, &p, 0);
	if (*p == '\0')
		return (uid_t) uid;

	return -1;
}

/* realm of this node */
char *krb5_this_realm;

static int gss_get_provided_realm(char *realm)
{
	if (krb5_this_realm)
		return 0;

	if (!realm)
		return -1;

	krb5_this_realm = strdup(realm);
	return 0;
}

static int gss_get_local_realm(void)
{
	krb5_context context = NULL;
	krb5_error_code code;

	if (krb5_this_realm != NULL)
		return 0;

	code = krb5_init_context(&context);
	if (code)
		return code;

	code = krb5_get_default_realm(context, &krb5_this_realm);
	krb5_free_context(context);

	if (code)
		return code;

	return 0;
}

int gss_get_realm(char *realm)
{
	int rc;

	/* Try to use provided realm first.
	 * If no provided realm, get default local realm.
	 */
	rc = gss_get_provided_realm(realm);
	if (rc)
		rc = gss_get_local_realm();

	return rc;
}
