/*****************************************************************************\
 *  callerid.c - Identify initiator of ssh connections, etc
 *****************************************************************************
 *  Copyright (C) 2015, Brigham Young University
 *  Author:  Ryan Cox <ryan_cox@byu.edu>
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm 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; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm 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.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#include "config.h"

#define _GNU_SOURCE

#ifdef __FreeBSD__
#include <sys/socket.h>
#include <netinet/in.h>
#endif

/*
 * FIXME: In in6.h, s6_addr32 def is guarded by #ifdef _KERNEL
 * Is there a portable interface that could be used instead of accessing
 * structure members directly?
 */
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__)
#define s6_addr32 __u6_addr.__u6_addr32
#endif

#include <arpa/inet.h>
#include <ctype.h>
#include <dirent.h>
#include <inttypes.h>
#include <libgen.h>
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "slurm/slurm.h"
#include "src/common/callerid.h"
#include "src/common/log.h"
#include "src/common/xstring.h"
#include "src/common/xmalloc.h"

#ifndef _BSD_SOURCE
#define _BSD_SOURCE
#endif

#ifndef PATH_PROCNET_TCP
#define PATH_PROCNET_TCP "/proc/net/tcp"
#endif
#ifndef PATH_PROCNET_TCP6
#define PATH_PROCNET_TCP6 "/proc/net/tcp6"
#endif

strong_alias(callerid_get_own_netinfo, slurm_callerid_get_own_netinfo);

static int _match_inode(callerid_conn_t *conn_result, ino_t *inode_search,
		callerid_conn_t *conn_row, ino_t inode_row, int af)
{
	if (*inode_search == inode_row) {
		memcpy(&conn_result->ip_dst, &conn_row->ip_dst, 16);
		memcpy(&conn_result->ip_src, &conn_row->ip_src, 16);
		conn_result->port_src = conn_row->port_src;
		conn_result->port_dst = conn_row->port_dst;
		conn_result->af = af;
		debug3("_match_inode matched");
		return SLURM_SUCCESS;
	}
	return SLURM_ERROR;
}

static int _match_conn(callerid_conn_t *conn_search, ino_t *inode_result,
		callerid_conn_t *conn_row, ino_t inode_row, int af)
{
	int addrbytes = af == AF_INET ? 4 : 16;

	if (conn_search->port_dst != conn_row->port_dst ||
	    conn_search->port_src != conn_row->port_src ||
	    memcmp((void*)&conn_search->ip_dst, (void*)&conn_row->ip_dst,
		   addrbytes) !=0 ||
	    memcmp((void*)&conn_search->ip_src, (void*)&conn_row->ip_src,
		   addrbytes) !=0
	   )
		return SLURM_ERROR;

	debug3("_match_conn matched inode %lu", (long unsigned int)inode_row);
	*inode_result = inode_row;
	return SLURM_SUCCESS;
}

/* Note that /proc/net/tcp, etc. can be updated *while reading* but a read on
 * each row is atomic: http://stackoverflow.com/a/5880485.
 *
 * This should be safe but may potentially miss an entry due to the entry
 * moving up in the file as it's read.
 */
static int _find_match_in_tcp_file(
		callerid_conn_t *conn,
		ino_t *inode,
		int af,
		const char *path,
		int (*match_func)(callerid_conn_t *,
				   ino_t *, callerid_conn_t *, ino_t, int))
{
	int rc = SLURM_ERROR;
	FILE *fp;
	char ip_dst_str[INET6_ADDRSTRLEN+1]; /* +1 for scanf to add \0 */
	char ip_src_str[INET6_ADDRSTRLEN+1];
	char line[1024];
	int addrbytes, i, matches;
	uint64_t inode_row;
	callerid_conn_t conn_row;

	addrbytes = af == AF_INET ? 4 : 16;

	/* Zero out the IPs. Not strictly necessary but it will look much better
	 * in a debugger since IPv4 only uses 4 out of 16 bytes. */
	memset(&conn_row.ip_dst, 0, 16);
	memset(&conn_row.ip_src, 0, 16);

	fp = fopen(path, "r");
	if (!fp)
		return rc;

	while( fgets(line, 1024, fp) != NULL ) {
		matches = sscanf(line,
			"%*s %[0-9A-Z]:%x %[0-9A-Z]:%x %*s %*s %*s %*s %*s %*s %"PRIu64"",
			ip_dst_str, &conn_row.port_dst, ip_src_str,
			&conn_row.port_src, &inode_row);

		if (matches == EOF)
			break;

		/* Probably the header */
		if (!matches)
			continue;

		/* Convert to usable forms */
		inet_nsap_addr(ip_dst_str, (unsigned char*)&conn_row.ip_dst,
				addrbytes);
		inet_nsap_addr(ip_src_str, (unsigned char*)&conn_row.ip_src,
				addrbytes);

		/* Convert to network byte order. */
		for (i=0; i < (addrbytes>>2); i++) {
			conn_row.ip_dst.s6_addr32[i]
				= htonl(conn_row.ip_dst.s6_addr32[i]);
			conn_row.ip_src.s6_addr32[i]
				= htonl(conn_row.ip_src.s6_addr32[i]);
		}

		/* Check if we matched */
		rc = match_func(conn, inode, &conn_row, (ino_t)inode_row, af);
		if (rc == SLURM_SUCCESS) {
			char ip_src_str[INET6_ADDRSTRLEN];
			char ip_dst_str[INET6_ADDRSTRLEN];

			inet_ntop(af, &conn->ip_src, ip_src_str,
					INET6_ADDRSTRLEN);
			inet_ntop(af, &conn->ip_dst, ip_dst_str,
					INET6_ADDRSTRLEN);
			debug("network_callerid matched %s:%lu => %s:%lu with inode %lu",
			      ip_src_str, (long unsigned int)conn->port_src,
			      ip_dst_str, (long unsigned int)conn->port_dst,
			      (long unsigned int)inode);
			break;
		}
	}

	fclose(fp);
	return rc;
}


/* Search through /proc/$pid/fd/ symlinks for the specified inode
 *
 * All errors in this function should be silently ignored. Processes appear and
 * disappear all the time. It is natural for processes to disappear in between
 * operations such as readdir, stat, and others. We should detect errors but
 * not log them.
 */
static int _find_inode_in_fddir(pid_t pid, ino_t inode)
{
	DIR *dirp;
	struct dirent *entryp;
	char dirpath[1024];
	char fdpath[PATH_MAX];
	int rc = SLURM_ERROR;
	struct stat statbuf;

	if (snprintf(dirpath, 1024, "/proc/%d/fd", (pid_t)pid) >= 1024)
		return SLURM_ERROR;
	if ((dirp = opendir(dirpath)) == NULL) {
		return SLURM_ERROR;
	}

	while (1) {
		if (!(entryp = readdir(dirp)))
			break;
		/* Ignore . and .. */
		else if (!xstrncmp(entryp->d_name, ".", 1))
			continue;

		/* This is a symlink. Follow it to get destination's inode. */
		if (snprintf(fdpath, sizeof(fdpath), "%s/%s", dirpath,
			     entryp->d_name) >= sizeof(fdpath))
			continue;

		if (stat(fdpath, &statbuf) != 0)
			continue;
		if (statbuf.st_ino == inode) {
			debug3("_find_inode_in_fddir: found %lu at %s",
			       (long unsigned int)inode, fdpath);
			rc = SLURM_SUCCESS;
			break;
		}
	}

	closedir(dirp);
	return rc;
}


extern int callerid_find_inode_by_conn(callerid_conn_t conn, ino_t *inode)
{
	int rc;

	rc = _find_match_in_tcp_file(&conn, inode, AF_INET, PATH_PROCNET_TCP,
			_match_conn);
	if (rc == SLURM_SUCCESS)
		return SLURM_SUCCESS;

	rc = _find_match_in_tcp_file(&conn, inode, AF_INET6, PATH_PROCNET_TCP6,
			_match_conn);
	if (rc == SLURM_SUCCESS)
		return SLURM_SUCCESS;

	/* Add new protocols here if needed, such as UDP */

	return SLURM_ERROR;
}


extern int callerid_find_conn_by_inode(callerid_conn_t *conn, ino_t inode)
{
	int rc;

	rc = _find_match_in_tcp_file(conn, &inode, AF_INET, PATH_PROCNET_TCP,
			_match_inode);
	if (rc == SLURM_SUCCESS)
		return SLURM_SUCCESS;

	rc = _find_match_in_tcp_file(conn, &inode, AF_INET6, PATH_PROCNET_TCP6,
			_match_inode);
	if (rc == SLURM_SUCCESS)
		return SLURM_SUCCESS;

	/* Add new protocols here if needed, such as UDP */

	return SLURM_ERROR;
}


/* Read through /proc then read each proc's fd/ directory.
 *
 * Most errors in this function should be silently ignored. Processes appear and
 * disappear all the time. It is natural for processes to disappear in between
 * operations such as readdir, stat, and others. We should detect errors but
 * not log them.
 */
extern int find_pid_by_inode (pid_t *pid_result, ino_t inode)
{
	DIR *dirp;
	struct dirent *entryp;
	char *dirpath = "/proc";
	int rc = SLURM_ERROR;
	pid_t pid;

	if ((dirp = opendir(dirpath)) == NULL) {
		/* Houston, we have a problem: /proc is inaccessible */
		error("find_pid_by_inode: unable to open %s: %m",
				dirpath);
		return SLURM_ERROR;
	}

	while (1) {
		if (!(entryp = readdir(dirp)))
			break;
		/* We're only looking for /proc/[0-9]*  */
		else if (!isdigit(entryp->d_name[0]))
			continue;

		/* More sanity checks can be performed but there isn't much
		 * point. The fd/ directory will exist inside the directory and
		 * we'll find the specified inode or we won't. Failures are
		 * silent so it won't clutter logs. The above checks are
		 * currently sufficient for Linux. */

		pid = (int)atoi(entryp->d_name);
		rc = _find_inode_in_fddir(pid, inode);
		if (rc == SLURM_SUCCESS) {
			*pid_result = pid;
			break;
		}
	}

	closedir(dirp);
	return rc;
}


extern int callerid_get_own_netinfo (callerid_conn_t *conn)
{
	DIR *dirp;
	struct dirent *entryp;
	char *dirpath = "/proc/self/fd";
	char fdpath[PATH_MAX];
	int rc = SLURM_ERROR;
	struct stat statbuf;

	if ((dirp = opendir(dirpath)) == NULL) {
		error("%s: opendir failed for %s: %m", __func__, dirpath);
		return rc;
	}

	while (1) {
		if (!(entryp = readdir(dirp)))
			break;
		/* Ignore . and .. */
		else if (!xstrncmp(entryp->d_name, ".", 1))
			continue;

		if (snprintf(fdpath, PATH_MAX, "%s/%s", dirpath,
			     entryp->d_name) >= PATH_MAX)
			continue;
		debug3("%s: checking %s", __func__, fdpath);
		/* This is a symlink. Follow it to get destination's inode. */
		if (stat(fdpath, &statbuf) != 0) {
			debug3("stat failed for %s: %m", fdpath);
			continue;
		}

		/* We are only interested in sockets */
		if (S_ISSOCK(statbuf.st_mode)) {
			debug3("%s: checking socket %s", __func__, fdpath);
			rc = callerid_find_conn_by_inode(conn, statbuf.st_ino);
			if (rc == SLURM_SUCCESS) {
				break;
			}
		}
	}

	closedir(dirp);
	return rc;
}
