#include <sys/types.h>
#include <sys/socket.h>
#include <sys/utsname.h>
#include <sys/mount.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

/* Default path we try to mount. "%s" gets replaced by our IP address */
#define NFS_ROOT		"/tftpboot/%s"
#define NFS_DEF_FILE_IO_BUFFER_SIZE	4096
#define NFS_MAXPATHLEN	1024
#define NFS_MNT_PROGRAM	100005
#define NFS_MNT_PORT	627
#define NFS_PROGRAM	100003
#define NFS_PORT	2049
#define NFS2_VERSION		2
#define NFS3_VERSION		3
#define NFS_MNT_PROGRAM		100005
#define NFS_MNT_VERSION		1
#define NFS_MNT3_VERSION	3
#define MNTPROC_MNT		1
#define MOUNTPROC3_MNT		1
#define RPC_PMAP_PROGRAM	100000
#define RPC_PMAP_VERSION	2
#define RPC_PMAP_PORT		111

#define NFS2_FHSIZE	32
#define NFS3_FHSIZE	64

#define RPC_VERSION 2

enum rpc_msg_type {
	RPC_CALL = 0,
	RPC_REPLY = 1
};

enum rpc_auth_flavor {
	RPC_AUTH_NULL  = 0,
	RPC_AUTH_UNIX  = 1,
	RPC_AUTH_SHORT = 2,
	RPC_AUTH_DES   = 3,
	RPC_AUTH_KRB   = 4,
};

enum rpc_reply_stat {
	RPC_MSG_ACCEPTED = 0,
	RPC_MSG_DENIED = 1
};

#define NFS_MAXFHSIZE		64
struct nfs_fh {
	unsigned short		size;
	unsigned char		data[NFS_MAXFHSIZE];
};

struct nfs2_fh {
	char			data[NFS2_FHSIZE];
};

#define NFS_MOUNT_VERSION	4

struct nfs_mount_data {
	int		version;
	int		fd;
	struct nfs2_fh	old_root;
	int		flags;
	int		rsize;
	int		wsize;
	int		timeo;
	int		retrans;
	int		acregmin;
	int		acregmax;
	int		acdirmin;
	int		acdirmax;
	struct sockaddr_in addr;
	char		hostname[256];
	int		namlen;
	unsigned int	bsize;
	struct nfs_fh	root;
};

#define NFS_MOUNT_SOFT		0x0001	/* 1 */
#define NFS_MOUNT_INTR		0x0002	/* 1 */
#define NFS_MOUNT_SECURE	0x0004	/* 1 */
#define NFS_MOUNT_POSIX		0x0008	/* 1 */
#define NFS_MOUNT_NOCTO		0x0010	/* 1 */
#define NFS_MOUNT_NOAC		0x0020	/* 1 */
#define NFS_MOUNT_TCP		0x0040	/* 2 */
#define NFS_MOUNT_VER3		0x0080	/* 3 */
#define NFS_MOUNT_KERBEROS	0x0100	/* 3 */
#define NFS_MOUNT_NONLM		0x0200	/* 3 */
#define NFS_MOUNT_BROKEN_SUID	0x0400	/* 4 */
#define NFS_MOUNT_FLAGMASK	0xFFFF

static char nfs_root_name[256];
static u_int32_t root_server_addr;
static char root_server_path[256];

/* Address of NFS server */
static u_int32_t servaddr;

/* Name of directory to mount */
static char nfs_path[NFS_MAXPATHLEN];

/* NFS-related data */
static struct nfs_mount_data nfs_data = {
	.version  =	NFS_MOUNT_VERSION,
	.flags    =	NFS_MOUNT_NONLM,	/* No lockd in nfs root yet */
	.rsize    =	NFS_DEF_FILE_IO_BUFFER_SIZE,
	.wsize    =	NFS_DEF_FILE_IO_BUFFER_SIZE,
	.bsize	  =	0,
	.timeo    =	7,
	.retrans  =	3,
	.acregmin =	3,
	.acregmax =	60,
	.acdirmin =	30,
	.acdirmax =	60,
};
static int nfs_port = -1;
static int mount_port;

/***************************************************************************

			     Parsing of options

 ***************************************************************************/

/*
 *  The following integer options are recognized
 */
static struct nfs_int_opts {
	const char *name;
	int  *val;
} root_int_opts[] = {
	{ "port",	&nfs_port },
	{ "rsize",	&nfs_data.rsize },
	{ "wsize",	&nfs_data.wsize },
	{ "timeo",	&nfs_data.timeo },
	{ "retrans",	&nfs_data.retrans },
	{ "acregmin",	&nfs_data.acregmin },
	{ "acregmax",	&nfs_data.acregmax },
	{ "acdirmin",	&nfs_data.acdirmin },
	{ "acdirmax",	&nfs_data.acdirmax },
	{ NULL,		NULL }
};

/*
 *  And now the flag options
 */
static struct nfs_bool_opts {
	const char *name;
	int  and_mask;
	int  or_mask;
} root_bool_opts[] = {
	{ "soft",	~NFS_MOUNT_SOFT,	NFS_MOUNT_SOFT },
	{ "hard",	~NFS_MOUNT_SOFT,	0 },
	{ "intr",	~NFS_MOUNT_INTR,	NFS_MOUNT_INTR },
	{ "nointr",	~NFS_MOUNT_INTR,	0 },
	{ "posix",	~NFS_MOUNT_POSIX,	NFS_MOUNT_POSIX },
	{ "noposix",	~NFS_MOUNT_POSIX,	0 },
	{ "cto",	~NFS_MOUNT_NOCTO,	0 },
	{ "nocto",	~NFS_MOUNT_NOCTO,	NFS_MOUNT_NOCTO },
	{ "ac",		~NFS_MOUNT_NOAC,	0 },
	{ "noac",	~NFS_MOUNT_NOAC,	NFS_MOUNT_NOAC },
	{ "lock",	~NFS_MOUNT_NONLM,	0 },
	{ "nolock",	~NFS_MOUNT_NONLM,	NFS_MOUNT_NONLM },
#ifdef CONFIG_NFS_V3
	{ "v2",		~NFS_MOUNT_VER3,	0 },
	{ "v3",		~NFS_MOUNT_VER3,	NFS_MOUNT_VER3 },
#endif
	{ "udp",	~NFS_MOUNT_TCP,		0 },
	{ "tcp",	~NFS_MOUNT_TCP,		NFS_MOUNT_TCP },
	{ "broken_suid",~NFS_MOUNT_BROKEN_SUID,	NFS_MOUNT_BROKEN_SUID },
	{ NULL,		0,			0 }
};
/*
 *  Parse option string.
 */
static void root_nfs_parse(char *name, char *buf)
{
	char *options, *val, *cp;

	if ((options = strchr(name, ','))) {
		*options++ = 0;
		cp = strtok(options, ",");
		while (cp) {
			if ((val = strchr(cp, '='))) {
				struct nfs_int_opts *opts = root_int_opts;
				*val++ = '\0';
				while (opts->name && strcmp(opts->name, cp))
					opts++;
				if (opts->name)
					*(opts->val) = (int) strtoul(val, NULL, 10);
			} else {
				struct nfs_bool_opts *opts = root_bool_opts;
				while (opts->name && strcmp(opts->name, cp))
					opts++;
				if (opts->name) {
					nfs_data.flags &= opts->and_mask;
					nfs_data.flags |= opts->or_mask;
				}
			}
			cp = strtok(NULL, ",");
		}
	}
	if (name[0] && strcmp(name, "default")) {
		strncpy(buf, name, NFS_MAXPATHLEN-1);
		buf[NFS_MAXPATHLEN-1] = 0;
	}
}

/*
 *  Prepare the NFS data structure and parse all options.
 */
static int root_nfs_name(char *name)
{
	char buf[NFS_MAXPATHLEN];
	struct utsname uname_buf;

	/* Set some default values */
	strcpy(buf, NFS_ROOT);

	/* Process options received from the remote server */
	root_nfs_parse(root_server_path, buf);

	/* Override them by options set on kernel command-line */
	root_nfs_parse(name, buf);

	uname(&uname_buf);
	if (strlen(buf) + strlen(uname_buf.nodename) > NFS_MAXPATHLEN) {
		printf("nfsroot: Pathname for remote directory too long.\n");
		return -1;
	}
	sprintf(nfs_path, buf, uname_buf.nodename);

	return 1;
}

/***************************************************************************

	       Routines to actually mount the root directory

 ***************************************************************************/

/*
 *  Construct sockaddr_in from address and port number.
 */
static inline void
set_sockaddr(struct sockaddr_in *sin, u_int32_t addr, u_int16_t port)
{
	memset(sin, 0, sizeof(*sin));
	sin->sin_family = AF_INET;
	sin->sin_addr.s_addr = addr;
	sin->sin_port = port;
}

/*
 * Extremely crude RPC-over-UDP call. We get an already encoded request
 * to pass, we do that and put the reply into buffer. That (and callers
 * below - getport, getfh2 and getfh3) should be replaced with proper
 * librpc use. Now, if we only had one that wasn't bloated as a dead
 * gnu that had lied for a while under the sun...
 */

static u_int32_t XID;
static int flag;
static void timeout(int n)
{
	(void)n;
	flag = 1;
}
static int do_call(struct sockaddr_in *sin, u_int32_t msg[], u_int32_t rmsg[],
		u_int32_t len, u_int32_t rlen)
{
	struct sockaddr_in from;
	int slen = sizeof(struct sockaddr_in);
	struct timeval tv = {1, 0};
	int n;
	int fd;

	sysv_signal(SIGALRM, timeout);
	fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (fd < 0)
		goto Esocket;
	setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (void*)&tv, sizeof(tv));
	len *= 4;
	if (sendto(fd, msg, len, 0, (struct sockaddr *)sin, slen)!=(int)len)
		goto Esend;
	setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (void*)&tv, sizeof(tv));
	alarm(0);
	flag = 0;
	alarm(5);
	rlen *= 4;
	do  {
		slen = sizeof(from);
		n = recvfrom(fd, rmsg, rlen, 0, (struct sockaddr*)&from, &slen);
		if (flag || n < 0)
			goto Erecv;
	} while (memcmp(&from, sin, sizeof(from)) || rmsg[0] != msg[0]);

	if (n < 6*4 || n % 4 || ntohl(rmsg[1]) != 1 || rmsg[2] ||
					rmsg[3] || rmsg[4] || rmsg[5])
		goto Einval;
	alarm(0);
	close(fd);
	return n / 4 - 6;

Esend:	printf("rpc: write failed\n");
	goto out;
Erecv:	printf("rpc: read failed\n");
	goto out;
Einval:	printf("rpc: invalid response\n");
	goto out;
Esocket:printf("rpc: can't create socket\n");
	return -1;
out:
	alarm(0);
	close(fd);
	return -1;
}

enum {
	PMAP_GETPORT = 3
};

static void do_header(u_int32_t msg[], u_int32_t prog, u_int32_t vers, u_int32_t proc)
{
	msg[0] = XID++;
	msg[1] = htonl(RPC_CALL);
	msg[2] = htonl(RPC_VERSION);
	msg[3] = htonl(prog);
	msg[4] = htonl(vers);
	msg[5] = htonl(proc);
	msg[6] = htonl(RPC_AUTH_NULL);
	msg[7] = htonl(0);
	msg[8] = htonl(RPC_AUTH_NULL);
	msg[9] = htonl(0);
}

static int getport(u_int32_t prog, u_int32_t vers, u_int32_t prot)
{
	struct sockaddr_in sin;
	unsigned msg[14];
	unsigned rmsg[7];
	int n;
	set_sockaddr(&sin, servaddr, htons(RPC_PMAP_PORT));
	do_header(msg, RPC_PMAP_PROGRAM, RPC_PMAP_VERSION, PMAP_GETPORT);
	msg[10] = htonl(prog);
	msg[11] = htonl(vers);
	msg[12] = htonl(prot);
	msg[13] = htonl(0);
	n = do_call(&sin, msg, rmsg, 14, 7);
	if (n <= 0)
		return -1;
	else
		return ntohl(rmsg[6]);
}

static int getfh2(void)
{
	struct sockaddr_in sin;
	unsigned msg[10+1+256/4];
	unsigned rmsg[6 + 1 + NFS2_FHSIZE/4];
	int n;
	int len = strlen(nfs_path);
	set_sockaddr(&sin, servaddr, mount_port);

	if (len > 255) {
		printf("nfsroot: pathname is too long");
		return -1;
	}
	memset(msg, 0, sizeof(msg));
	do_header(msg, NFS_MNT_PROGRAM, NFS_MNT_VERSION, MNTPROC_MNT);
	msg[10] = htonl(len);
	strcpy((char*)&msg[11], nfs_path);
	n = do_call(&sin, msg, rmsg, 11 + (len + 3)/4, 7 + NFS2_FHSIZE/4);
	if (n < 0)
		return -1;
	if (n != NFS2_FHSIZE/4 + 1)
		goto Esize;
	if (rmsg[6]) {
		printf("nfsroot: mountd returned an error (%d)",htonl(rmsg[6]));
		return -1;
	}
	nfs_data.root.size = NFS2_FHSIZE;
	memcpy(nfs_data.root.data, &rmsg[7], NFS2_FHSIZE);
	return 0;
Esize:
	printf("nfsroot: bad fhandle size");
	return -1;
}

static int getfh3(void)
{
	struct sockaddr_in sin;
	unsigned msg[10+1+256/4];
	unsigned rmsg[6 + 1 + 1 + NFS3_FHSIZE/4];
	int n;
	int len = strlen(nfs_path);
	int size;
	set_sockaddr(&sin, servaddr, mount_port);

	if (len > 255) {
		printf("nfsroot: pathname is too long");
		return -1;
	}
	memset(msg, 0, sizeof(msg));
	do_header(msg, NFS_MNT_PROGRAM, NFS_MNT3_VERSION, MOUNTPROC3_MNT);
	msg[10] = htonl(len);
	strcpy((char*)&msg[11], nfs_path);
	n = do_call(&sin, msg, rmsg, 11 + (len + 3)/4, 8 + NFS3_FHSIZE/4);
	if (n < 0)
		return -1;
	if (n <= 2)
		goto Esize;
	if (rmsg[6]) {
		printf("nfsroot: mountd returned an error (%d)",htonl(rmsg[6]));
		return -1;
	}
	size = ntohl(rmsg[7]);
	if (size > NFS3_FHSIZE || n != 2 + size/4)
		goto Esize;
	nfs_data.root.size = size;
	memcpy(nfs_data.root.data, &rmsg[8], size);
	return 0;
Esize:
	printf("nfsroot: bad fhandle size");
	return -1;
}

/*
 *  Use portmapper to find mountd and nfsd port numbers if not overriden
 *  by the user. Use defaults if portmapper is not available.
 *  XXX: Is there any nfs server with no portmapper?
 */
static int root_nfs_ports(void)
{
	int port;
	int nfsd_ver, mountd_ver;
	int proto;

	if (nfs_data.flags & NFS_MOUNT_VER3) {
		nfsd_ver = NFS3_VERSION;
		mountd_ver = NFS_MNT3_VERSION;
	} else {
		nfsd_ver = NFS2_VERSION;
		mountd_ver = NFS_MNT_VERSION;
	}

	proto = (nfs_data.flags & NFS_MOUNT_TCP) ? IPPROTO_TCP : IPPROTO_UDP;

	if (nfs_port < 0) {
		if ((port = getport(NFS_PROGRAM, nfsd_ver, proto)) < 0) {
			printf("nfsroot: Unable to get nfsd port "
					"number from server, using default\n");
			port = NFS_PORT;
		}
		nfs_port = htons(port);
		printf("nfsroot: Portmapper on server returned %d "
			"as nfsd port\n", port);
	}

	if ((port = getport(NFS_MNT_PROGRAM, mountd_ver, proto)) < 0) {
		printf("nfsroot: Unable to get mountd port "
				"number from server, using default\n");
		port = NFS_MNT_PORT;
	}
	mount_port = htons(port);
	printf("nfsroot: mountd port is %d\n", port);

	return 0;
}

int main(void)
{
	unsigned char *p;
	struct timeval tv;
	char *s;

	/* FIX: use getopt() instead of this */

	s = getenv("root_server_addr");
	if (s)
		root_server_addr = strtoul(s, NULL, 10);
	s = getenv("root_server_path");
	if (s)
		strncpy(root_server_path, s, 255);
	s = getenv("nfs_root_name");
	if (s)
		strncpy(nfs_root_name, s, 255);

	/*
	 * Decode the root directory path name and NFS options from
	 * the kernel command line. This has to go here in order to
	 * be able to use the client IP address for the remote root
	 * directory (necessary for pure RARP booting).
	 */
	if (root_nfs_name(nfs_root_name) < 0)
		return 0;
	if ((servaddr = root_server_addr) == INADDR_NONE) {
		printf("nfsroot: No NFS server available, giving up.\n");
		return 0;
	}

	p = (char *) &servaddr;
	sprintf(nfs_data.hostname, "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);

#ifdef NFSROOT_DEBUG
	printf("nfsroot: Mounting %s on server %s as root\n",
		nfs_path, nfs_data.hostname);
	printf("nfsroot:     rsize = %d, wsize = %d, timeo = %d, retrans = %d\n",
		nfs_data.rsize, nfs_data.wsize, nfs_data.timeo, nfs_data.retrans);
	printf("nfsroot:     acreg (min,max) = (%d,%d), acdir (min,max) = (%d,%d)\n",
		nfs_data.acregmin, nfs_data.acregmax,
		nfs_data.acdirmin, nfs_data.acdirmax);
	printf("nfsroot:     nfsd port = %d, mountd port = %d, flags = %08x\n",
		nfs_port, mount_port, nfs_data.flags);
#endif

	gettimeofday(&tv, NULL);
	XID = (tv.tv_sec << 15) ^ tv.tv_usec;

	if (root_nfs_ports() < 0)
		return 0;
	if (nfs_data.flags & NFS_MOUNT_VER3) {
		if (getfh3())
			return 0;
	} else {
		if (getfh2())
			return 0;
	}
	set_sockaddr((struct sockaddr_in *) &nfs_data.addr, servaddr, nfs_port);
	return mount("/dev/root", "/mnt", "nfs", 0, &nfs_data) == 0;
}
