/* Copyright (C) 2002 Mitsuru KANDA
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/param.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <syslog.h>
#include <signal.h>

#include "nl.h"
#include "chap.h"
#include "dtcp.h"
#include "util.h"
#include "rtsol.h"
#include "version.h"

static char cvs_id[] = "$Id: dtcpc.c,v 1.19 2002/08/01 05:58:35 mk Exp $";
int sockfd = -1;
struct tunnel_config *tc;
int sit_status = 0;
int send_rs = 0;


/* "host", "network", "tunnelonly" */
#define DEFAULT_TUNTYPE    "host"
#define DEFAULT_INTERVAL   180 /* 'ping' interval. 300 sec is too long */
#define DEFAULT_TIMEOUT    60
#define DEFAULT_DTCPC_DEV  "dtcp"
#define DEFAULT_NET_DEV    "eth0"
#define DTCPC_PID_FILE     "/var/run/dtcpc.pid"

static int do_connect(const char *server, const char *service);
static void dtcpc_poll(int interval);
static void do_ping(int signo);
static void do_quit(int signum);
static int dtcpc_quit(void);
static void dtcpc_close(void);
static int mktunnel(int sockfd, struct chap_id *pci, char *tuntype);
static int up_tunnel(void);
static int down_tunnel(void);
static void do_kill(void);

static const char *shortopt = "u:p:t:n:i:c:rvkdh";
enum {
	__OPT_MIN = 0xff,
	OPT_USER,
	OPT_PORT,
	OPT_TUNNEL,
	OPT_NETDEV,
	OPT_RS,
	OPT_IF,
	OPT_CYCLE,
	OPT_VERSION,
	OPT_KILL,
	OPT_DEBUG,
	OPT_HELP,
	__OPT_MAX
};

static struct option const longopt[] = 
{
	{"user",             required_argument, NULL, OPT_USER},
	{"port",             required_argument, NULL, OPT_PORT},
	{"tunnel",           required_argument, NULL, OPT_TUNNEL},
	{"network-device",   required_argument, NULL, OPT_NETDEV},
	{"send-rs",          no_argument,       NULL, OPT_RS},
	{"interface",        required_argument, NULL, OPT_IF},
	{"cycle",            required_argument, NULL, OPT_CYCLE},
	{"version",          no_argument,       NULL, OPT_VERSION},
	{"kill",             no_argument,       NULL, OPT_KILL},
	{"debug",            no_argument,       NULL, OPT_DEBUG},
	{"help",             no_argument,       NULL, OPT_HELP},
	{0,                  0,                  0,         0}
};

void
usage(void)
{
	fprintf(stdout,
		"usage: \n"
		"\tdtcpc [options] dtcp-server\n"
		"\toptions:\n"
		"\t       -i dev: specify interfac ename(default: dtcp)\n"
		"\t       -d: debug\n"
		"\t       -u: username\n"
		"\t       -p port: connect port instead of default\n"
		"\t       -t tunnel type: specify tunnel type (default 'host')\n"
		"\t       -n netdev (default 'eth0')\n"
		"\t       -r : send RS\n"
		"\t       -c sec: \"ping\" cycle (sec)\n"
		"\tdtcpc -k : kill dtcpc\n"
		"\tdtcpc -h : display this help\n"
		"\tdtcpc -v : display version\n"
		);
}

void
display_version(void)
{
	fprintf(stdout,
		"\tversion: %s\n"
		"\tcvs id: %s\n",
		MKDTCP_VERSION, 
		cvs_id
		);
}

int
main(int argc, char **argv)
{
	int c;
	int rtn;
	int opt_index;
	char *servername = NULL;
	char strport[6]; /* short */
	struct chap_id ci;
	char  *user = NULL;
	char tuntype[16] = DEFAULT_TUNTYPE;
	int interval = DEFAULT_INTERVAL;
	pid_t pid;

	debug = 0;
	memset(&tc, 0, sizeof(tc));
	tc = (struct tunnel_config *)malloc(sizeof(struct tunnel_config));
	snprintf(strport, sizeof(strport), "%d", DTCP_PORT);
	strncpy(tc->dev, DEFAULT_DTCPC_DEV, strlen(DEFAULT_DTCPC_DEV));
	strncpy(tc->netdev, DEFAULT_NET_DEV, strlen(DEFAULT_NET_DEV));

	if (argc < 2) { /* minimum: dtcpc -k or dtcpc server */
		usage();
		exit(EXIT_FAILURE);
	}

	while ((c = getopt_long(argc, argv, shortopt, longopt, &opt_index)) != EOF) {
		switch (c) {
		case 'u':
		case OPT_USER:
			user = (char *)malloc(strlen(optarg)+1);
			strncpy(user, optarg, strlen(optarg)+1);
			ci.username = user;
			break;
		case 'p':
		case OPT_PORT:
			strncpy(strport, optarg, sizeof(strport)-1);
			if (debug) fprintf(stdout, "port: %s\n", strport);
			break;
		case 't':
		case OPT_TUNNEL:
			if (!strncasecmp(optarg, "host", strlen("host"))) {
				strcpy(tuntype, "host");
			} else if (!strncasecmp(optarg, "network", strlen("network"))) {
				strcpy(tuntype, "network");
			} else if (!strncasecmp(optarg, "tunnelonly", strlen("tunnelonly"))) {
				strcpy(tuntype, "tunnelonly");
			} else {
				fprintf(stderr, "unknown tunnel type\n");
				exit(EXIT_FAILURE);
			}
			break;
		case 'n':
		case OPT_NETDEV:
			strncpy(tc->netdev, optarg, sizeof(tc->netdev)-1);
			break;
		case 'r':
		case OPT_RS:
			send_rs = 1;
			break;
		case 'i':
		case OPT_IF:
			if (optarg) {
				strncpy(tc->dev, optarg, sizeof(tc->dev)-1); 
			} else {
				usage();
				exit(EXIT_FAILURE);
			}
			break;
		case 'c':
		case OPT_CYCLE:
			interval = atoi(optarg);
			if (interval < 10) 
				interval = 10; /* minimum ! */
			break;
		case 'v':
		case OPT_VERSION:
			display_version();
			exit(EXIT_SUCCESS);
			break;
		case 'k':
		case OPT_KILL:
			do_kill();
			exit(EXIT_SUCCESS);
			break;
		case 'd':
		case OPT_DEBUG:
			debug=1;
			break;
		case 'h':
		case OPT_HELP:
		default:
			usage();
			exit(EXIT_SUCCESS);
			break;
		}
	}

	if (optind < argc) {
		servername = argv[optind];
		if (debug) fprintf(stdout, "servername: %s\n", servername);
	}

	openlog("dtcpc", LOG_PID, LOG_DAEMON);

	if (!servername) {
		usage();
		exit(EXIT_FAILURE);
	}

	if (debug) fprintf(stdout, "dev: %s\n", tc->dev);
	
	sockfd = do_connect(servername, strport);
	if (sockfd < 0) {
		exit(EXIT_FAILURE);
	}

	signal(SIGINT, do_quit);
	signal(SIGPIPE, do_quit);
	signal(SIGTERM, do_quit);

	if (!user) {
		user = getlogin();
	}
	ci.username = user;
	if (debug) printf("User: %s\n", ci.username);

	ci.password = getpass("DTCP Account Password > ");
	if (debug) printf("Password: %s\n", ci.password);

	ci.challenge = get_chap_challenge(sockfd);
	if (ci.challenge == NULL) {
		exit(EXIT_FAILURE);
	}
	if (debug) printf("chap challenge: %s\n", ci.challenge);

	chap_response(&ci);
	if (debug) { 
		int i;
		printf("chap response: ");
		for (i=0; i<CHAP_HASH_LEN;i++)
			printf("%02X", ci.hash[i]);
		printf("\n");
	}

	if (mktunnel(sockfd, &ci, tuntype)<0) {
		exit(EXIT_FAILURE);
	}
	
	if (!debug) {
		rtn = daemon(0, 0);
		if (rtn < 0) {
			perror("daemon");
			exit(EXIT_FAILURE);
		}
		umask(0);
		pid = getpid();
		if (write_pid(DTCPC_PID_FILE, pid) < 0) {
			syslog(LOG_ERR, "can't write pid file\n");
			exit(EXIT_FAILURE);
		}
	}

	dtcpc_poll(interval);

	closelog();
	close(sockfd);
	return 0;
}

static void
do_kill()
{
	pid_t pid;

	pid = read_pid(DTCPC_PID_FILE);
	if (pid < 0) {
		fprintf(stderr, "can't read pidfile\n");
		return ;
	}
	kill(pid, SIGTERM);

	unlink(DTCPC_PID_FILE);
}

static void
dtcpc_poll(int interval)
{

	syslog(LOG_INFO, "now completed\n");

	signal(SIGALRM, do_ping);

	while (1) {
		alarm(interval);
		if (debug) syslog(LOG_DEBUG, "set alarm(%d)\n", interval);
		pause();
	}

	/* open control socket */
	/* setitimer, ping */
}

/* service: port number string */
static int
do_connect(const char *server, const char *service)
{
	int sockfd;
	struct addrinfo hints, *res, *ressave;
	int rtn;

	if (debug) printf("service:%s\n", service);
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;

	if ((rtn = getaddrinfo(server, service, &hints, &res)) != 0) {
		syslog(LOG_ERR, "dtcp_connect: %s\n", gai_strerror(rtn));
		return -1;
	}

	ressave = res;
	do {
		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd < 0)
			continue;
		if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
			break;
		close(sockfd);
	} while ((res = res->ai_next) != NULL);

	if (res == NULL) {
		syslog(LOG_ERR, "dtcp_connect: can't connect %s:%s\n", server, service);
		return -1;
	}

	freeaddrinfo(ressave);

	return sockfd;
}

static void
do_ping(int signo)
{
        char msg[]= "ping\r\n";
        int len = strlen(msg);
        int rtn = 0;
        char recvbuf[BUFSIZ];
        
        rtn = do_write(sockfd, msg, len);
        if (rtn != len) {
		syslog(LOG_ERR, "can't write message\n");
                dtcpc_quit();/* or just ignore ? */
        }

        if (tgetline(sockfd, recvbuf, sizeof(recvbuf), DEFAULT_TIMEOUT) < 0) {
		syslog(LOG_ERR, "can't receive answer\n");
		dtcpc_quit();
	}
	trim(recvbuf, strlen(recvbuf)+1);

        rtn = do_rcvchk(recvbuf);
        if (rtn == DTCP_ERR) {
		syslog(LOG_ERR, "server returned -ERR\n");
		dtcpc_close();
	}
	syslog(LOG_PID, "PING to dtcp server.\n");
        return;
}

static void
do_quit(int signum)
{
	if (sockfd < 0) 
		dtcpc_close();
	else
		dtcpc_quit();

	exit(EXIT_SUCCESS);
}

static int
dtcpc_quit()
{
        char msg[]= "QUIT\r\n";
        int len = strlen(msg);
        int rtn = 0;
        char recvbuf[BUFSIZ];
        
        rtn = do_write(sockfd, msg, len);
        if (rtn != len) {
                return -1;
        }

        if (tgetline(sockfd, recvbuf, sizeof(recvbuf), DEFAULT_TIMEOUT) < 0)
                return -1;

	trim(recvbuf, strlen(recvbuf)+1);
        rtn = do_rcvchk(recvbuf);
        if (rtn == DTCP_ERR)
                return -1;

	syslog(LOG_PID, "QUIT to dtcp server.\n");

	dtcpc_close();

	return 0;
}

static void
dtcpc_close()
{
	down_tunnel();
	close(sockfd);
	syslog(LOG_INFO, "session closed.\n");
}

/* 
 * tunnel <username> <chap-response> <tunnel-type>
 * tunnel fred 7DDBED2073AD8FA5B43E3DC4CFF62045 host 
 *
 * +OK 1.2.3.4 5.6.7.8 3ffe:1:1:1::1 3ffe:1:1:1::2 
 */
static int
mktunnel(int sockfd, struct chap_id *pci, char *tuntype)
{
	int rtn = 0;
	int len = 0;
	int err;
	char msg[BUFSIZ];
	char *p;
	int ttype =  detect_tuntype(tuntype);

	len = snprintf(msg, sizeof(msg), "tunnel %s %s %s\r\n", 
		       pci->username, pci->hash, tuntype);

	if (debug) syslog(LOG_DEBUG, "%s", msg);

	if (len != strlen(msg)) {
		syslog(LOG_ERR, "mktunnel: msg len mismatch\n");
		return -1;
	}

	rtn = do_write(sockfd, msg, len);
	if (rtn != len) {
		syslog(LOG_ERR, "mktunnel: write len mismatch\n");
		return -1;
	}

	if (getline(sockfd, msg, sizeof(msg)) < 0) {
		syslog(LOG_ERR, "mktunnel: getline failed\n");
		return -1;
	}
	if (debug) syslog(LOG_DEBUG, "%s", msg);

	trim(msg, strlen(msg));
	p = strtok(msg, " \t");
	if (p) {
		if (do_rcvchk(p) != DTCP_OK) {
			syslog(LOG_ERR, "mktunnel:server returned ERR\n");
			if (debug) syslog(LOG_ERR, "%s", msg);
			return -1;
		}
	} else {
		return -1;
	}

	/* parse addresses */
	tc->in_c = (struct in_addr *)malloc(sizeof(struct in_addr));
	if (!tc->in_c) return -1;
	p = strtok(NULL, " \t");
	if (p) {
		syslog(LOG_INFO, "IPv4 Client: %s\n", p);
		inet_pton(AF_INET, p, tc->in_c);
	} else {
		return -1;
	}

	tc->in_s = (struct in_addr *)malloc(sizeof(struct in_addr));
	if (!tc->in_s) return -1;
	p = strtok(NULL, " \t");
	if (p) {
		syslog(LOG_INFO, "IPv4 Server: %s\n", p);
		inet_pton(AF_INET, p, tc->in_s);
	} else {
		return -1;
	}

	switch (ttype) {
	case DTCP_HOST: /* "(+OK ipv4-c ipv4-s )ipv6-c ipv6-s" */
		syslog(LOG_INFO, "TYPE: HOST\n");
		tc->type = DTCP_HOST;
		tc->in6_c = (struct in6_addr *)malloc(sizeof(struct in6_addr));
		if (!tc->in6_c) return -1;
		p = strtok(NULL, " \t");
		if (p) {
			syslog(LOG_INFO, "IPv6 Client: %s\n", p);
			inet_pton(AF_INET6, p, tc->in6_c);
		} else {
			return -1;
		}
		tc->in6_s = (struct in6_addr *)malloc(sizeof(struct in6_addr));
		if (!tc->in6_s) return -1;
		p = strtok(NULL, " \t");
		if (p) {
			syslog(LOG_INFO, "IPv6 Server: %s\n", p);
			inet_pton(AF_INET6, p, tc->in6_s);
		} else {
			return -1;
		}
		err = up_tunnel();
		break;
	case DTCP_NETWORK: /* "(+OK ipv4-c ipv4-s )ipv6-n/prefx" */
		syslog(LOG_INFO, "TYPE: NETWORK\n");		
		tc->type = DTCP_NETWORK;
		tc->in6_net = (struct in6_addr *)malloc(sizeof(struct in6_addr));
		p = strtok(NULL, "/");
		if (p) {
			syslog(LOG_INFO, "IPv6 Network: %s/", p);
			inet_pton(AF_INET6, p, tc->in6_net);
		} else {
			return -1;
		}
		p = strtok(NULL, "/");
		if (p) {
			syslog(LOG_INFO, "%s\n", p);
			tc->plen = atoi(p);
		} else {
			return -1;
		}
		if (tc->plen <= 0) {
			syslog(LOG_ERR, "server returned invalid prefix len\n");
			return -1;
		}
		err = up_tunnel();
		break;
	case DTCP_TUNNEL_ONLY: /* "(+OK ipv4-c ipv4-s)" */
		syslog(LOG_INFO, "TYPE: TUNNELONLY\n");		
		tc->type = DTCP_TUNNEL_ONLY;
		err = up_tunnel();
		break;
	default:
		err = -1;
		break;
	}

	return err;
}
static int
up_tunnel()
{
	struct in6_dst_room in6dr;

	/* this part is checked more verbosely */
	if (link_check("sit0", IFF_UP)) {
		sit_status = 0;
		if (debug) syslog(LOG_DEBUG, "sit0: up\n");
		link_set("sit0", 1);
	} else {
		sit_status = 1; /* already up'ed */
	}

	if  (link_check("sit0", IFF_UP)) {
		syslog(LOG_ERR, "sit0: can't up\n");
		return -1;
	}
	if (link_set_mtu("sit0", 1280)) {
		syslog(LOG_ERR, "sit0: can't set mtu 1280\n");
		return -1;
	}

	if (!link_check(tc->dev, 0)) { /* exist */
		if (!link_check(tc->dev, IFF_UP)) {
			syslog(LOG_ERR, "%s: already up'ed, why?\n", tc->dev);
			return -1;
		} else {
			if (link_set(tc->dev, 1)) {
				syslog(LOG_ERR, "%s: can't up\n", tc->dev);
				return -1;
			}
		}
	} else {
		if (sit_set(tc->dev, tc->in_s, 1)) {
			syslog(LOG_ERR, "%s: can't create\n", tc->dev);
			return -1;
		}
		link_set(tc->dev, 1);
	}

	if (link_check(tc->dev, IFF_UP)) {
		syslog(LOG_ERR, "%s: can't up\n", tc->dev);
		return -1;
	}
	if (link_set_mtu(tc->dev, 1280)) {
		syslog(LOG_ERR, "%s:can't set mtu 1280\n", tc->dev);
		return -1;
	}


	switch (tc->type) {
	case DTCP_HOST:
		ipaddr6_set(tc->dev, tc->in6_c, 64, 1);
		in6dr.gw = NULL;
		in6dr.plen = 0;
		if (rt6_set(tc->dev, &in6dr, 1)) {
			syslog(LOG_ERR, "can't set default gw\n");
			return -1;
		}
		syslog(LOG_PID, "set default gw dev %s\n", tc->dev);
		break;
	case DTCP_NETWORK: /* XXX, you must set forwarding=1 via sysctl */
		tc->in6_c = (struct in6_addr *)malloc(sizeof(struct in6_addr));
		memcpy(tc->in6_c, tc->in6_net, sizeof(*tc->in6_net));
		tc->in6_c->s6_addr[15] = 1;/* XXX */
		ipaddr6_set(tc->netdev, tc->in6_c, 64, 1);
		in6dr.gw = NULL;
		in6dr.plen = 0;
		if (rt6_set(tc->dev, &in6dr, 1)) {
			syslog(LOG_ERR, "can't set default gw\n");
			return -1;
		}
		in6dr.gw = NULL;
		in6dr.range = tc->in6_net;
		in6dr.plen = (char)tc->plen; 
		if (rt6_set(tc->netdev, &in6dr, 1)) {
			syslog(LOG_ERR, "can't set route\n");
			return -1;
		}
		break;
	/* sit device can receive RS, (use rtsol)
	 * but can't assing ipv6 address automatically */
	case DTCP_TUNNEL_ONLY: 
		/* DO NOTHING ? - I don't know :-( */
		if (send_rs) {
			if (do_rtsol(tc->in6_c)<0) {
				syslog(LOG_ERR, "rtsol failed\n.");
			}
		}
		break;
	default: 
		return -1;
	}
	
	return 0;
}
	
static int
down_tunnel()
{
	struct in6_dst_room in6dr;

	switch (tc->type) {
	case DTCP_HOST:
		ipaddr6_set(tc->dev, tc->in6_c, 64, 0);
		in6dr.gw = NULL;
		in6dr.plen = 0;
		rt6_set(tc->dev, &in6dr, 0);
		break;
	case DTCP_NETWORK:
		ipaddr6_set(tc->netdev, tc->in6_c, 64, 0);
		in6dr.gw = NULL;
		in6dr.plen = 0;
		rt6_set(tc->dev, &in6dr, 0);
		in6dr.gw = NULL;
		in6dr.range = tc->in6_net;
		in6dr.plen = (char)tc->plen;
		rt6_set(tc->netdev, &in6dr, 0);
		break;
	case DTCP_TUNNEL_ONLY:
		/* DO NOTHING ? - I don't know :-( */
		break;
	default: 
		return -1;
	}

	sit_set(tc->dev, NULL, 0);
	if (sit_status == 0)
		link_set("sit0", 0);
	/* sit_set("sit0", NULL, 0); */

	return 0;
}
