/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2002-2003  Maxim Krasnyansky <maxk@qualcomm.com>
 *  Copyright (C) 2002-2008  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 *  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; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <signal.h>
#include <getopt.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/l2cap.h>
#include <bluetooth/bnep.h>
#include <bluetooth/hidp.h>

#include "sdp.h"
#include "pand.h"

#ifdef NEED_PPOLL
#include "ppoll.h"
#endif

static uint16_t role    = BNEP_SVC_PANU;	/* Local role (ie service) */
static uint16_t service = BNEP_SVC_NAP;		/* Remote service */

static int detach = 1;
static int persist;
static int use_sdp = 1;
static int use_cache;
static int link_mode = 0;
static int cleanup;
static int search_duration = 10;

static struct {
	int      valid;
	char     dst[40];
	bdaddr_t bdaddr;
} cache;

static char netdev[16] = "bnep%d";
static char *pidfile = NULL;
static char *devupcmd = NULL;
static char *devdowncmd = NULL;

static bdaddr_t src_addr = *BDADDR_ANY;
static int src_dev = -1;

static volatile int terminate;

static void do_kill(char *dst);

enum {
	NONE,
	SHOW,
	LISTEN,
	CONNECT,
	KILL
} modes;

struct script_arg {
	char	dev[20];
	char	dst[20];
	int	sk;
	int	nsk;
};

static void run_script(char *script, char *dev, char *dst, int sk, int nsk)
{
	char *argv[4];
	struct sigaction sa;

	if (!script)
		return;

	if (access(script, R_OK | X_OK))
		return;

	if (fork())
		return;

	if (sk >= 0)
		close(sk);

	if (nsk >= 0)
		close(nsk);

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = SIG_DFL;
	sigaction(SIGCHLD, &sa, NULL);
	sigaction(SIGPIPE, &sa, NULL);

	argv[0] = script;
	argv[1] = dev;
	argv[2] = dst;
	argv[3] = NULL;

	execv(script, argv);

	exit(1);
}

/* Wait for disconnect or error condition on the socket */
static int w4_hup(int sk, struct script_arg *down_cmd)
{
	struct pollfd pf;
	sigset_t sigs;
	int n;

	sigfillset(&sigs);
	sigdelset(&sigs, SIGCHLD);
	sigdelset(&sigs, SIGPIPE);
	sigdelset(&sigs, SIGTERM);
	sigdelset(&sigs, SIGINT);
	sigdelset(&sigs, SIGHUP);

	while (!terminate) {
		pf.fd = sk;
		pf.events = POLLERR | POLLHUP;

		n = ppoll(&pf, 1, NULL, &sigs);

		if (n < 0) {
			if (errno == EINTR || errno == EAGAIN)
				continue;

			syslog(LOG_ERR, "Poll failed. %s(%d)",
						strerror(errno), errno);

			return 1;
		}

		if (n) {
			int err = 0;
			socklen_t olen = sizeof(err);

			getsockopt(sk, SOL_SOCKET, SO_ERROR, &err, &olen);

			syslog(LOG_INFO, "%s disconnected%s%s", netdev,
				err ? " : " : "", err ? strerror(err) : "");

			if (down_cmd)
				run_script(devdowncmd,
						down_cmd->dev, down_cmd->dst,
						down_cmd->sk, down_cmd->nsk);

			close(sk);

			return 0;
		}
	}

	return 0;
}

static int do_listen(void)
{
	struct l2cap_options l2o;
	struct sockaddr_l2 l2a;
	socklen_t olen;
	int sk, lm;

	if (use_sdp)
		bnep_sdp_register(&src_addr, role);

	/* Create L2CAP socket and bind it to PSM BNEP */
	sk = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
	if (sk < 0) {
		syslog(LOG_ERR, "Cannot create L2CAP socket. %s(%d)",
						strerror(errno), errno);
		return -1;
	}

	memset(&l2a, 0, sizeof(l2a));
	l2a.l2_family = AF_BLUETOOTH;
	bacpy(&l2a.l2_bdaddr, &src_addr);
	l2a.l2_psm = htobs(BNEP_PSM);

	if (bind(sk, (struct sockaddr *) &l2a, sizeof(l2a))) {
		syslog(LOG_ERR, "Bind failed. %s(%d)",
						strerror(errno), errno);
		return -1;
	}

	/* Setup L2CAP options according to BNEP spec */
	memset(&l2o, 0, sizeof(l2o));
	olen = sizeof(l2o);
	if (getsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &l2o, &olen) < 0) {
		syslog(LOG_ERR, "Failed to get L2CAP options. %s(%d)",
						strerror(errno), errno);
		return -1;
	}

	l2o.imtu = l2o.omtu = BNEP_MTU;
	if (setsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &l2o, sizeof(l2o)) < 0) {
		syslog(LOG_ERR, "Failed to set L2CAP options. %s(%d)",
						strerror(errno), errno);
		return -1;
	}

	/* Set link mode */
	lm = link_mode;
	if (lm && setsockopt(sk, SOL_L2CAP, L2CAP_LM, &lm, sizeof(lm)) < 0) {
		syslog(LOG_ERR, "Failed to set link mode. %s(%d)",
						strerror(errno), errno);
		return -1;
	}

	listen(sk, 10);

	while (!terminate) {
		socklen_t alen = sizeof(l2a);
		char devname[16];
		int nsk;

		nsk = accept(sk, (struct sockaddr *) &l2a, &alen);
		if (nsk < 0) {
			syslog(LOG_ERR, "Accept failed. %s(%d)",
						strerror(errno), errno);
			continue;
		}

		switch (fork()) {
		case 0:
			break;
		case -1:
			syslog(LOG_ERR, "Fork failed. %s(%d)",
						strerror(errno), errno);
		default:
			close(nsk);
			continue;
		}

		strncpy(devname, netdev, 16);
		devname[15] = '\0';

		if (!bnep_accept_connection(nsk, role, devname)) {
			char str[40];
			struct script_arg down_cmd;

			ba2str(&l2a.l2_bdaddr, str);

			syslog(LOG_INFO, "New connection from %s at %s",
								str, devname);

			run_script(devupcmd, devname, str, sk, nsk);

			memset(&down_cmd, 0, sizeof(struct script_arg));
			strncpy(down_cmd.dev, devname, strlen(devname) + 1);
			strncpy(down_cmd.dst, str, strlen(str) + 1);
			down_cmd.sk = sk;
			down_cmd.nsk = nsk;
			w4_hup(nsk, &down_cmd);
		} else {
			syslog(LOG_ERR, "Connection failed. %s(%d)",
						strerror(errno), errno);
		}

		close(nsk);
		exit(0);
	}

	if (use_sdp)
		bnep_sdp_unregister();

	return 0;
}

/* Connect and initiate BNEP session
 * Returns:
 *   -1 - critical error (exit persist mode)
 *   1  - non critical error
 *   0  - success
 */
static int create_connection(char *dst, bdaddr_t *bdaddr)
{
	struct l2cap_options l2o;
	struct sockaddr_l2 l2a;
	socklen_t olen;
	int sk, r = 0;
	struct script_arg down_cmd;

	syslog(LOG_INFO, "Connecting to %s", dst);

	sk = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
	if (sk < 0) {
		syslog(LOG_ERR, "Cannot create L2CAP socket. %s(%d)",
						strerror(errno), errno);
		return -1;
	}

	/* Setup L2CAP options according to BNEP spec */
	memset(&l2o, 0, sizeof(l2o));
	olen = sizeof(l2o);
	getsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &l2o, &olen);
	l2o.imtu = l2o.omtu = BNEP_MTU;
	setsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &l2o, sizeof(l2o));

	memset(&l2a, 0, sizeof(l2a));
	l2a.l2_family = AF_BLUETOOTH;
	bacpy(&l2a.l2_bdaddr, &src_addr);

	if (bind(sk, (struct sockaddr *) &l2a, sizeof(l2a)))
		syslog(LOG_ERR, "Bind failed. %s(%d)",
						strerror(errno), errno);

	memset(&l2a, 0, sizeof(l2a));
	l2a.l2_family = AF_BLUETOOTH;
	bacpy(&l2a.l2_bdaddr, bdaddr);
	l2a.l2_psm = htobs(BNEP_PSM);

	if (!connect(sk, (struct sockaddr *) &l2a, sizeof(l2a)) && 
			!bnep_create_connection(sk, role, service, netdev)) {

		syslog(LOG_INFO, "%s connected", netdev);

		run_script(devupcmd, netdev, dst, sk, -1);

		if (persist || devdowncmd) {
				memset(&down_cmd, 0, sizeof(struct script_arg));
				strncpy(down_cmd.dev, netdev, strlen(netdev) + 1);
				strncpy(down_cmd.dst, dst, strlen(dst) + 1);
				down_cmd.sk = sk;
				down_cmd.nsk = -1;
				w4_hup(sk, &down_cmd);

			if (terminate && cleanup) {
				syslog(LOG_INFO, "Disconnecting from %s.", dst);
				do_kill(dst);
			}
		}

		r = 0;
	} else {
		syslog(LOG_ERR, "Connect to %s failed. %s(%d)",
						dst, strerror(errno), errno);
		r = 1;
	}

	close(sk);

	if (use_cache) {
		if (!r) {
			/* Succesesful connection, validate cache */
			strcpy(cache.dst, dst);
			bacpy(&cache.bdaddr, bdaddr);
			cache.valid = use_cache;
		} else
			cache.valid--;
	}

	return r;
}

/* Search and connect
 * Returns:
 *   -1 - critical error (exit persist mode)
 *   1  - non critical error
 *   0  - success
 */
static int do_connect(void)
{
	inquiry_info *ii;
	int reconnect = 0;
	int i, n, r = 0;

	do {
		if (reconnect)
			sleep(persist);
		reconnect = 1;

		if (cache.valid > 0) {
			/* Use cached bdaddr */
			r = create_connection(cache.dst, &cache.bdaddr);
			if (r < 0) {
				terminate = 1;
				break;
			}
			continue;
		}

		syslog(LOG_INFO, "Inquiring");

		/* FIXME: Should we use non general LAP here ? */

		ii = NULL;
		n  = hci_inquiry(src_dev, search_duration, 0, NULL, &ii, 0);
		if (n < 0) {
			syslog(LOG_ERR, "Inquiry failed. %s(%d)",
						strerror(errno), errno);
			continue;
		}

		for (i = 0; i < n; i++) {
			char dst[40];
			ba2str(&ii[i].bdaddr, dst);

			if (use_sdp) {
				syslog(LOG_INFO, "Searching for %s on %s", 
						bnep_svc2str(service), dst);

				if (bnep_sdp_search(&src_addr, &ii[i].bdaddr, service) <= 0)
					continue;
			}

			r = create_connection(dst, &ii[i].bdaddr);
			if (r < 0) {
				terminate = 1;
				break;
			}
		}
		bt_free(ii);
	} while (!terminate && persist);

	return r;
}

static void do_show(void)
{
	bnep_show_connections();
}

static void do_kill(char *dst)
{
	if (dst)
		bnep_kill_connection((void *) strtoba(dst));
	else
		bnep_kill_all_connections();
}

static void sig_hup(int sig)
{
	return;
}

static void sig_term(int sig)
{
	terminate = 1;
}

static int write_pidfile(void)
{
	int fd;
	FILE *f;
	pid_t pid;

	do {
		fd = open(pidfile, O_WRONLY|O_TRUNC|O_CREAT|O_EXCL, 0644);
		if (fd == -1) {
			/* Try to open the file for read. */
			fd = open(pidfile, O_RDONLY);
			if (fd < 0) {
				syslog(LOG_ERR, "Could not read old pidfile: %s(%d)",
							strerror(errno), errno);
				return -1;
			}
			
			/* We're already running; send a SIGHUP (we presume that they
			 * are calling ifup for a reason, so they probably want to
			 * rescan) and then exit cleanly and let things go on in the
			 * background.  Muck with the filename so that we don't go
			 * deleting the pid file for the already-running instance.
			 */
			f = fdopen(fd, "r");
			if (!f) {
				syslog(LOG_ERR, "Could not fdopen old pidfile: %s(%d)",
							strerror(errno), errno);
				close(fd);
				return -1;
			}

			pid = 0;
			if (fscanf(f, "%d", &pid) != 1)
				pid = 0;
			fclose(f);

			if (pid) {
				/* Try to kill it. */
				if (kill(pid, SIGHUP) == -1) {
					/* No such pid; remove the bogus pid file. */
					syslog(LOG_INFO, "Removing stale pidfile");
					unlink(pidfile);
					fd = -1;
				} else {
					/* Got it.  Don't mess with the pid file on
					 * our way out. */
					syslog(LOG_INFO, "Signalling existing process %d and exiting\n", pid);
					pidfile = NULL;
					return -1;
				}
			}
		}
	} while(fd == -1);

	f = fdopen(fd, "w");
	if (!f) {
		syslog(LOG_ERR, "Could not fdopen new pidfile: %s(%d)",
						strerror(errno), errno);
		close(fd);
		unlink(pidfile);
		return -1;
	}

	fprintf(f, "%d\n", getpid());
	fclose(f);

	return 0;
}

static struct option main_lopts[] = {
	{ "help",     0, 0, 'h' },
	{ "listen",   0, 0, 's' },
	{ "connect",  1, 0, 'c' },
	{ "search",   2, 0, 'Q' },
	{ "kill",     1, 0, 'k' },
	{ "killall",  0, 0, 'K' },
	{ "role",     1, 0, 'r' },
	{ "service",  1, 0, 'd' },
	{ "ethernet", 1, 0, 'e' },
	{ "device",   1, 0, 'i' },
	{ "nosdp",    0, 0, 'D' },
	{ "list",     0, 0, 'l' },
	{ "show",     0, 0, 'l' },
	{ "nodetach", 0, 0, 'n' },
	{ "persist",  2, 0, 'p' },
	{ "auth",     0, 0, 'A' },
	{ "encrypt",  0, 0, 'E' },
	{ "secure",   0, 0, 'S' },
	{ "master",   0, 0, 'M' },
	{ "cache",    0, 0, 'C' },
	{ "pidfile",  1, 0, 'P' },
	{ "devup",    1, 0, 'u' },
	{ "devdown",  1, 0, 'o' },
	{ "autozap",  0, 0, 'z' },
	{ 0, 0, 0, 0 }
};

static const char *main_sopts = "hsc:k:Kr:d:e:i:lnp::DQ::AESMC::P:u:o:z";

static const char *main_help = 
	"Bluetooth PAN daemon version %s\n"
	"Usage:\n"
	"\tpand <options>\n"
	"Options:\n"
	"\t--show --list -l          Show active PAN connections\n"
	"\t--listen -s               Listen for PAN connections\n"
	"\t--connect -c <bdaddr>     Create PAN connection\n"
	"\t--autozap -z              Disconnect automatically on exit\n"
	"\t--search -Q[duration]     Search and connect\n"
	"\t--kill -k <bdaddr>        Kill PAN connection\n"
	"\t--killall -K              Kill all PAN connections\n"
	"\t--role -r <role>          Local PAN role (PANU, NAP, GN)\n"
	"\t--service -d <role>       Remote PAN service (PANU, NAP, GN)\n"
	"\t--ethernet -e <name>      Network interface name\n"
	"\t--device -i <bdaddr>      Source bdaddr\n"
	"\t--nosdp -D                Disable SDP\n"
	"\t--auth -A                 Enable authentication\n"
	"\t--encrypt -E              Enable encryption\n"
	"\t--secure -S               Secure connection\n"
	"\t--master -M               Become the master of a piconet\n"
	"\t--nodetach -n             Do not become a daemon\n"
	"\t--persist -p[interval]    Persist mode\n"
	"\t--cache -C[valid]         Cache addresses\n"
	"\t--pidfile -P <pidfile>    Create PID file\n"
	"\t--devup -u <script>       Script to run when interface comes up\n"
	"\t--devdown -o <script>     Script to run when interface comes down\n";

int main(int argc, char *argv[])
{
	char *dst = NULL, *src = NULL;
	struct sigaction sa;
	int mode = NONE;
	int opt;

	while ((opt=getopt_long(argc, argv, main_sopts, main_lopts, NULL)) != -1) {
		switch(opt) {
		case 'l':
			mode = SHOW;
			detach = 0;
			break;

		case 's':
			mode = LISTEN;
			break;

		case 'c':
			mode = CONNECT;
			dst  = strdup(optarg);
			break;

		case 'Q':
			mode = CONNECT;
			if (optarg)
				search_duration = atoi(optarg);
			break;

		case 'k':
			mode = KILL;
			detach = 0;
			dst  = strdup(optarg);
			break;

		case 'K':
			mode = KILL;
			detach = 0;
			break;

		case 'i':
			src = strdup(optarg);
			break;

		case 'r':
			bnep_str2svc(optarg, &role);
			break;

		case 'd':
			bnep_str2svc(optarg, &service);
			break;

		case 'D':
			use_sdp = 0;
			break;

		case 'A':
			link_mode |= L2CAP_LM_AUTH;
			break;

		case 'E':
			link_mode |= L2CAP_LM_ENCRYPT;
			break;

		case 'S':
			link_mode |= L2CAP_LM_SECURE;
			break;

		case 'M':
			link_mode |= L2CAP_LM_MASTER;
			break;

		case 'e':
			strncpy(netdev, optarg, 16);
			netdev[15] = '\0';
			break;

		case 'n':
			detach = 0;
			break;

		case 'p':
			if (optarg)
				persist = atoi(optarg);
			else
				persist = 5;
			break;

		case 'C':
			if (optarg)
				use_cache = atoi(optarg);
			else
				use_cache = 2;
			break;

		case 'P':
			pidfile = strdup(optarg);
			break;

		case 'u':
			devupcmd = strdup(optarg);
			break;

		case 'o':
			devdowncmd = strdup(optarg);
			break;

		case 'z':
			cleanup = 1;
			break;

		case 'h':
		default:
			printf(main_help, VERSION);
			exit(0);
		}
	}

	argc -= optind;
	argv += optind;
	optind = 0;

	if (bnep_init())
		return -1;

	/* Check non daemon modes first */
	switch (mode) {
	case SHOW:
		do_show();
		return 0;

	case KILL:
		do_kill(dst);
		return 0;

	case NONE:
		printf(main_help, VERSION);
		return 0;
	}

	/* Initialize signals */
	memset(&sa, 0, sizeof(sa));
	sa.sa_flags   = SA_NOCLDSTOP;
	sa.sa_handler = SIG_IGN;
	sigaction(SIGCHLD, &sa, NULL);
	sigaction(SIGPIPE, &sa, NULL);

	sa.sa_handler = sig_hup;
	sigaction(SIGHUP, &sa, NULL);

	sa.sa_handler = sig_term;
	sigaction(SIGTERM, &sa, NULL);
	sigaction(SIGINT,  &sa, NULL);

	if (detach && daemon(0, 0)) {
		perror("Can't start daemon");
		exit(1);
	}

	openlog("pand", LOG_PID | LOG_NDELAY | LOG_PERROR, LOG_DAEMON);
	syslog(LOG_INFO, "Bluetooth PAN daemon version %s", VERSION);

	if (src) {
		src_dev = hci_devid(src);
		if (src_dev < 0 || hci_devba(src_dev, &src_addr) < 0) {
			syslog(LOG_ERR, "Invalid source. %s(%d)",
						strerror(errno), errno);
			return -1;
		}
	}

	if (pidfile && write_pidfile())
		return -1;

	if (dst) {
		/* Disable cache invalidation */
		use_cache = 0;

		strncpy(cache.dst, dst, sizeof(cache.dst) - 1);
		str2ba(dst, &cache.bdaddr);
		cache.valid = 1;
		free(dst);
	}

	switch (mode) {
	case CONNECT:
		do_connect();
		break;

	case LISTEN:
		do_listen();
		break;
	}

	if (pidfile)
		unlink(pidfile);

	return 0;
}
