/*
 * Copyright 2011 Oracle.  All rights reserved.
 *
 * This file is part of fedfs-utils.
 *
 * fedfs-utils is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2.0 as
 * published by the Free Software Foundation.
 *
 * fedfs-utils 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 version 2.0 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * version 2.0 along with fedfs-utils.  If not, see:
 *
 *	http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/mount.h>
#include <sys/wait.h>

#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <libgen.h>
#include <errno.h>
#include <getopt.h>
#include <locale.h>
#include <netdb.h>
#include <langinfo.h>

#include "fedfs-nls.h"
#include "fedfs-getsrvinfo.h"
#include "fedfs-gpl-boiler.h"

/**
 * Top-level directory on client under which we mount NFSv4 domain roots
 */
#define FEDFS_NFS4_TLDIR		"nfs4"

/**
 * Name of SRV record containing NFSv4 FedFS root
 */
#define FEDFS_NFS_DOMAINROOT	"_nfs-domainroot._tcp"

/**
 * Export path of NFSv4 FedFS root
 */
#define FEDFS_NFS_EXPORTPATH	"/.domainroot"

/**
 * Pathname to mount.nfs
 */
#define MOUNT_NFS_EXECUTABLE		"/sbin/mount.nfs"

/**
 * Mount status values, lifted from util-linux
 */
enum {
	EX_SUCCESS	= 0,
	EX_USAGE	= 1,
	EX_FAIL		= 32,
};

static char *progname;
static int nomtab;
static int verbose;
static _Bool readonly;
static _Bool sloppy;
static _Bool fake;

/**
 * Short form command line options
 */
static const char fedfs_opts[] = "fhno:rsvVw";

/**
 * Long form command line options
 */
static const struct option fedfs_longopts[] =
{
	{ "fake", 0, NULL, 'f' },
	{ "help", 0, NULL, 'h' },
	{ "no-mtab", 0, NULL, 'n' },
	{ "options", 1, NULL, 'o' },
	{ "read-only", 0, NULL, 'r' },
	{ "read-write", 0, NULL, 'w' },
	{ "ro", 0, NULL, 'r' },
	{ "rw", 0, NULL, 'w' },
	{ "sloppy", 0, NULL, 's' },
	{ "verbose", 0, NULL, 'v' },
	{ "version", 0, NULL, 'V' },
	{ NULL, 0, NULL, 0 }
};

/**
 * Display mount.fedfs usage message
 */
static void
mount_usage(void)
{
	printf(_("\nUsage: %s remotedir localdir [-fhnrsvVw]\n\n"), progname);
	printf(_("options:\n"));
	printf(_("\t-f\t\tFake mount, do not actually mount\n"));
	printf(_("\t-h\t\tPrint this help\n"));
	printf(_("\t-n\t\tDo not update /etc/mtab\n"));
	printf(_("\t-r\t\tMount file system readonly\n"));
	printf(_("\t-s\t\tTolerate sloppy mount options\n"));
	printf(_("\t-v\t\tVerbose\n"));
	printf(_("\t-V\t\tPrint version\n"));
	printf(_("\t-w\t\tMount file system read-write\n"));

	printf("%s", fedfs_gpl_boilerplate);
}

/**
 * Concatenate three strings
 *
 * @param s NUL-terminated C string
 * @param t NUL-terminated C string
 * @param u NUL-terminated C string
 * @return pointer to NUL-terminated C string or NULL
 *
 * Caller must free the returned string with free(3).  Always frees
 * its first arg - typical use: s = xstrconcat3(s,t,u);
 *
 * Lifted from util-linux.
 */
static char *
xstrconcat3(char *s, const char *t, const char *u)
{
	_Bool free_s = true;
	char *result;

	if (s == NULL) {
		s = "";
		free_s = false;
	}
	if (t == NULL)
		t = "";
	if (u == NULL)
		u = "";
	result = malloc(strlen(s) + strlen(t) + strlen(u) + 1);
	if (result == NULL)
		goto out;

	strcpy(result, s);
	strcat(result, t);
	strcat(result, u);

out:
	if (free_s)
		free(s);
	return result;
}

/**
 * Exec mount.nfs
 *
 * @param server NUL-terminated C string containing name of NFS server
 * @param port server port to use when mounting
 * @param domainname NUL-terminated C string containing FedFS domain name
 * @param export_path NUL-terminated C string containing server export path
 * @param mounted_on_dir NUL-terminated C string containing local mounted-on directory
 * @param text_options NUL-terminated C string containing user's mount options
 *
 */
static void
exec_mount_nfs4(const char *server, unsigned short port,
		const char *domainname, const char *export_path,
		const char *mounted_on_dir, const char *text_options)
{
	static char special[2048];
	static char options[2048];
	char *args[16];
	int count = 0;

	snprintf(special, sizeof(special), "%s:%s/%s%s", server,
			FEDFS_NFS_EXPORTPATH, domainname, export_path);

	if (text_options != NULL && strcmp(text_options, "") != 0)
		snprintf(options, sizeof(options), "%s,vers=4,fg,port=%u",
				text_options, port);
	else
		snprintf(options, sizeof(options), "vers=4,fg,port=%u", port);

	if (verbose) {
		printf(_("%s: Special device:       %s\n"),
			progname, special);
		printf(_("%s: Mounted-on directory: %s\n"),
			progname, mounted_on_dir);
		printf(_("%s: Mount options:        %s\n"),
			progname, options);
	}

	args[count++] = MOUNT_NFS_EXECUTABLE;
	args[count++] = special;
	args[count++] = (char *)mounted_on_dir;
	if (verbose)
		args[count++] = "-v";
	if (fake)
		args[count++] = "-f";
	if (nomtab)
		args[count++] = "-n";
	if (readonly)
		args[count++] = "-r";
	if (sloppy)
		args[count++] = "-s";
	args[count++] = "-o";
	args[count++] = options;

	args[count] = NULL;
	execv(args[0], args);
}

/**
 * Mount a FedFS domain root via NFSv4
 *
 * @param domainname NUL-terminated C string containing FedFS domain name
 * @param export_path NUL-terminated C string containing server export path
 * @param mounted_on_dir NUL-terminated C string containing local mounted-on directory
 * @param text_options NUL-terminated C string containing user's mount options
 * @return exit status code from the mount.nfs command
 *
 * Construct the server:/export string and the mounted-on directory string
 * based on the DNS SRV query results, then fork and exec mount.nfs to do
 * the actual mount request.
 */
static int
nfs4_mount(const char *domainname, const char *export_path,
		const char *mounted_on_dir, const char *text_options)
{
	struct srvinfo *tmp, *si = NULL;
	int error, status;

	status = EX_FAIL;

	error = getsrvinfo(FEDFS_NFS_DOMAINROOT, domainname, &si);
	switch (error) {
	case ESI_SUCCESS:
		break;
	case ESI_NONAME:
		fprintf(stderr, _("%s: Domain name %s not found\n"),
			progname, domainname);
		goto out;
	case ESI_SERVICE:
		fprintf(stderr, _("%s: No FedFS domain root available for %s\n"),
			progname, domainname);
		goto out;
	default:
		fprintf(stderr, _("%s: Failed to resolve %s: %s\n"),
			progname, domainname, gsi_strerror(error));
		goto out;
	}

	/*
	 * The srvinfo list is already in RFC 2782 sorted order.  Try each
	 * SRV record once, in the foreground.  Go with the first one that
	 * works.
	 */
	for (tmp = si; tmp != NULL; tmp = tmp->si_next) {
		pid_t pid;

		pid = fork();
		switch (pid) {
		case 0:
			exec_mount_nfs4(tmp->si_target, tmp->si_port,
					domainname, export_path, mounted_on_dir,
					text_options);
			/*NOTREACHED*/
			fprintf(stderr, _("%s: Failed to exec: %s\n"),
				progname, strerror(errno));
			exit(EX_FAIL);
		case -1:
			fprintf(stderr, _("%s: Failed to fork: %s\n"),
				progname, strerror(errno));
			goto out;
		default:
			waitpid(pid, &status, 0);
			if (status == EX_SUCCESS)
				goto out;
		}
	}

out:
	freesrvinfo(si);
	return status;
}

/**
 * Try one mount request
 *
 * @param source NUL-terminated C string containing name of "special device"
 * @param target NUL-terminated C string containing local mounted-on directory
 * @param text_options NUL-terminated C string containing user's mount options
 * @return an exit status code
 *
 * Parse the pathname in "source."  It contains the file system protocol
 * and FedFS domain name.  Then pass these arguments to the appropriate
 * mount helper subcommand.
 */
static int
try_mount(const char *source, const char *target, const char *text_options)
{
	char *global_name, *topdir, *domainname, *remaining;
	int result;

	remaining = NULL;
	result = EX_FAIL;

	global_name = strdup(source);
	if (global_name == NULL) {
		fprintf(stderr, _("%s: Unable to parse globally useful name\n"),
				progname);
		goto out;
	}

	topdir = strtok(global_name, "/");
	if (topdir == NULL) {
		fprintf(stderr, _("%s: Invalid globally useful name: %s\n"),
			progname, source);
		goto out;
	}
	if (verbose)
		printf(_("%s: Top-level directory:  %s\n"),
			progname, topdir);

	domainname = strtok(NULL, "/");
	if (domainname == NULL) {
		fprintf(stderr, _("%s: Missing domain name in globally "
				"useful name: %s\n"), progname, source);
		goto out;
	}
	if (verbose)
		printf(_("%s: Domain name:          %s\n"),
			progname, domainname);

	remaining = strtok(NULL, "/");
	if (remaining == NULL) {
		remaining = strdup("/");
		if (remaining == NULL) {
			fprintf(stderr, _("%s: No memory\n"), progname);
			goto out;
		}
	} else {
		char *tmp;

		tmp = malloc(strlen(remaining) + 1);
		if (tmp == NULL) {
			fprintf(stderr, _("%s: No memory\n"), progname);
			remaining = NULL;
			goto out;
		}
		strcpy(tmp, "/");
		strcat(tmp, remaining);
		remaining = tmp;
	}
	if (verbose)
		printf(_("%s: Export path:          %s\n"),
			progname, remaining);

	if (strcmp(topdir, FEDFS_NFS4_TLDIR) == 0)
		result = nfs4_mount(domainname, remaining, target, text_options);
#if 0
	/* example: SMB support plugs in here */
	else if (strcmp(topdir, FEDFS_SMB_TLDIR) == 0)
		result = smb_mount(domainname, remaining, target, text_options);
#endif
	else
		fprintf(stderr, _("%s: Unrecognized file system protocol\n"), progname);

out:
	free(global_name);
	free(remaining);

	return result;
}

/**
 * Program entry point
 *
 * @param argc count of command line arguments
 * @param argv array of NUL-terminated C strings containing command line arguments
 * @return program exit status
 */
int main(int argc, char *argv[])
{
	char *source, *target, *text_options;
	int c, mnt_err;

	(void)setlocale(LC_ALL, "");

	progname = basename(argv[0]);

	if (argv[1] && argv[1][0] == '-') {
		if(argv[1][1] == 'V')
			printf("%s (VERSION_STRING)\n", progname);
		else
			mount_usage();
		exit(EX_SUCCESS);
	}

	if (argc < 3) {
		mount_usage();
		exit(EX_USAGE);
	}

	source = argv[1];
	target = argv[2];

	mnt_err = EX_USAGE;
	text_options = NULL;
	readonly = false;
	sloppy = false;
	fake = false;
	argv[2] = argv[0]; /* so that getopt error messages are correct */
	while ((c = getopt_long(argc - 2, argv + 2, fedfs_opts,
				fedfs_longopts, NULL)) != -1) {
		switch (c) {
		case 'f':
			fake = true;
			break;
		case 'n':
			++nomtab;
			break;
		case 'o':
			/* Ugh. */
			if (text_options != NULL)
				text_options = xstrconcat3(text_options, ",", optarg);
			else
				text_options = strdup(optarg);
			if (text_options == NULL) {
				fprintf(stderr, _("%s: No memory\n"), progname);
				goto out;
			}
			break;
		case 'r':
			readonly = true;
			break;
		case 's':
			sloppy = true;
			break;
		case 'v':
			++verbose;
			break;
		case 'V':
			printf("%s: (VERSION_STRING)\n", progname);
			mnt_err = EX_SUCCESS;
			goto out;
		case 'w':
			readonly = false;
			break;
		case 'h':
		default:
			mount_usage();
			goto out;
		}
	}

	/* Extra non-option words at the end are bogus...  */
	if (optind != argc - 2) {
		mount_usage();
		goto out;
	}

	if (getuid() != 0 && geteuid() != 0) {
		fprintf(stderr, _("%s: Not installed setuid - "
			    "\"user\" FedFS mounts are not supported\n"), progname);
		mnt_err = EX_FAIL;
		goto out;
	}

	mnt_err = try_mount(source, target, text_options);

out:
	free(text_options);
	exit(mnt_err);
}
