/* ip.access nanoBTS configuration tool */

/* (C) 2009-2010 by Harald Welte <laforge@gnumonks.org>
 * (C) 2017 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <getopt.h>
#include <time.h>
#include <talloc.h>
#include <errno.h>

#include <osmocom/core/select.h>
#include <osmocom/core/timer.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/gsm/protocol/ipaccess.h>
#include <osmocom/gsm/ipa.h>
#include <osmocom/bsc/gsm_data.h>

static struct {
	const char *ifname;
	const char *bind_ip;
	int send_interval;
	bool list_view;
	time_t list_view_timeout;
	bool format_json;
} cmdline_opts = {
	.ifname = NULL,
	.bind_ip = NULL,
	.send_interval = 5,
	.list_view = false,
	.list_view_timeout = 10,
	.format_json = false,
};

static void print_help()
{
	printf("\n");
	printf("Usage: abisip-find [-l] [<interface-name>]\n");
	printf("  <interface-name>  Specify the outgoing network interface,\n"
	       "                    e.g. 'eth0'\n");
	printf("  -b --bind-ip <ip> Specify the local IP to bind to,\n"
	       "                    e.g. '192.168.1.10'\n");
	printf("  -i --interval <s> Send broadcast frames every <s> seconds.\n");
	printf("  -l --list-view    Instead of printing received responses,\n"
	       "                    output a sorted list of currently present\n"
	       "                    base stations and change events.\n");
	printf("  -t --timeout <s>  Drop base stations after <s> seconds of\n"
	       "                    receiving no more replies from it.\n"
	       "                    Implies --list-view.\n");
	printf("  -j --format-json  Print BTS information using json syntax.\n");
}

static void handle_options(int argc, char **argv)
{
	while (1) {
		int option_index = 0, c;
		static struct option long_options[] = {
			{"help", 0, 0, 'h'},
			{"bind-ip", 1, 0, 'b'},
			{"send-interval", 1, 0, 'i'},
			{"list-view", 0, 0, 'l'},
			{"timeout", 1, 0, 't'},
			{"format-json", 0, 0, 'j'},
			{0, 0, 0, 0}
		};

		c = getopt_long(argc, argv, "hb:i:lt:j",
				long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 'h':
			print_help();
			exit(EXIT_SUCCESS);
		case 'b':
			cmdline_opts.bind_ip = optarg;
			break;
		case 'i':
			errno = 0;
			cmdline_opts.send_interval = strtoul(optarg, NULL, 10);
			if (errno || cmdline_opts.send_interval < 1) {
				fprintf(stderr, "Invalid interval value: %s\n", optarg);
				exit(EXIT_FAILURE);
			}
			break;
		case 't':
			errno = 0;
			cmdline_opts.list_view_timeout = strtoul(optarg, NULL, 10);
			if (errno) {
				fprintf(stderr, "Invalid timeout value: %s\n", optarg);
				exit(EXIT_FAILURE);
			}
			/* fall through to imply list-view: */
		case 'l':
			cmdline_opts.list_view = true;
			break;
		case 'j':
			cmdline_opts.format_json = true;
			break;
		default:
			/* catch unknown options *as well as* missing arguments. */
			fprintf(stderr, "Error in command line options. Exiting. Try --help.\n");
			exit(EXIT_FAILURE);
			break;
		}
	}

	if (argc - optind > 0)
		cmdline_opts.ifname = argv[optind++];

	if (argc - optind > 0) {
		fprintf(stderr, "Error: too many arguments\n");
		print_help();
		exit(EXIT_FAILURE);
	}
}

static int udp_sock(const char *ifname, const char *bind_ip)
{
	int fd, rc, bc = 1;
	struct sockaddr_in sa;

	fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (fd < 0)
		return fd;

	if (ifname) {
#ifdef __FreeBSD__
		rc = setsockopt(fd, SOL_SOCKET, IP_RECVIF, ifname,
				strlen(ifname));
#else
		rc = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname,
				strlen(ifname));
#endif
		if (rc < 0)
			goto err;
	}

	memset(&sa, 0, sizeof(sa));
	sa.sin_family = AF_INET;
	sa.sin_port = htons(3006);
	if (bind_ip) {
		rc = inet_pton(AF_INET, bind_ip, &sa.sin_addr);
		if (rc != 1) {
			fprintf(stderr, "bind ip addr: inet_pton failed, returned %d\n", rc);
			goto err;
		}
	} else {
		sa.sin_addr.s_addr = INADDR_ANY;
	}

	rc = bind(fd, (struct sockaddr *)&sa, sizeof(sa));
	if (rc < 0)
		goto err;

	rc = setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &bc, sizeof(bc));
	if (rc < 0)
		goto err;

#if 0
	/* we cannot bind, since the response packets don't come from
	 * the broadcast address */
	sa.sin_family = AF_INET;
	sa.sin_port = htons(3006);
	inet_aton("255.255.255.255", &sa.sin_addr);

	rc = connect(fd, (struct sockaddr *)&sa, sizeof(sa));
	if (rc < 0)
		goto err;
#endif
	return fd;

err:
	close(fd);
	return rc;
}

const unsigned char find_pkt[] = { 0x00, 0x0b+8, IPAC_PROTO_IPACCESS, 0x00,
				IPAC_MSGT_ID_GET,
					0x01, IPAC_IDTAG_MACADDR,
					0x01, IPAC_IDTAG_IPADDR,
					0x01, IPAC_IDTAG_UNIT,
					0x01, IPAC_IDTAG_LOCATION1,
					0x01, IPAC_IDTAG_LOCATION2,
					0x01, IPAC_IDTAG_EQUIPVERS,
					0x01, IPAC_IDTAG_SWVERSION,
					0x01, IPAC_IDTAG_UNITNAME,
					0x01, IPAC_IDTAG_SERNR,
				};


static int bcast_find(int fd)
{
	struct sockaddr_in sa;

	sa.sin_family = AF_INET;
	sa.sin_port = htons(3006);
	inet_aton("255.255.255.255", &sa.sin_addr);

	return sendto(fd, find_pkt, sizeof(find_pkt), 0, (struct sockaddr *) &sa, sizeof(sa));
}

static char *parse_response(void *ctx, unsigned char *buf, int len)
{
	unsigned int out_len;
	uint8_t t_len;
	uint8_t t_tag;
	uint8_t *cur = buf;
	char *out = talloc_zero_size(ctx, 512);

	if (cmdline_opts.format_json)
		out = talloc_asprintf_append(out,"{ ");

	while (cur < buf + len) {
		t_len = *cur++;
		t_tag = *cur++;

		if (cmdline_opts.format_json)
			out = talloc_asprintf_append(out, "\"%s\": \"%s\", ", ipa_ccm_idtag_name(t_tag), cur);
		else
			out = talloc_asprintf_append(out, "%s='%s'  ", ipa_ccm_idtag_name(t_tag), cur);

		cur += t_len;
	}

	if (cmdline_opts.format_json) {
		out_len = strlen(out);
		if (out[out_len-2] == ',')
			out[out_len-2] = ' ';
		out[out_len-1] = '}';
	}

	return out;
}

struct base_station {
	struct llist_head entry;
	char *line;
	time_t timestamp;
};

LLIST_HEAD(base_stations);

void *ctx = NULL;

void print_timestamp()
{
	time_t now = time(NULL);
	printf("\n\n----- %s\n", ctime(&now));
}

struct base_station *base_station_parse(unsigned char *buf, int len)
{
	struct base_station *new_bs = talloc_zero(ctx, struct base_station);
	new_bs->line = parse_response(new_bs, buf, len);
	new_bs->timestamp = time(NULL);
	return new_bs;
}

bool base_stations_add(struct base_station *new_bs)
{
	struct base_station *bs;

	llist_for_each_entry(bs, &base_stations, entry) {
		int c = strcmp(new_bs->line, bs->line);
		if (!c) {
			/* entry already exists. */
			bs->timestamp = new_bs->timestamp;
			return false;
		}

		if (c < 0) {
			/* found the place to add the entry */
			break;
		}
	}

	print_timestamp();
	printf("New:\n%s\n", new_bs->line);

	llist_add_tail(&new_bs->entry, &bs->entry);
	return true;
}

bool base_stations_timeout()
{
	struct base_station *bs, *next_bs;
	time_t now = time(NULL);
	bool changed = false;

	llist_for_each_entry_safe(bs, next_bs, &base_stations, entry) {
		if (now - bs->timestamp < cmdline_opts.list_view_timeout)
			continue;
		print_timestamp();
		printf("LOST:\n%s\n", bs->line);

		llist_del(&bs->entry);
		talloc_free(bs);
		changed = true;
	}
	return changed;
}

void base_stations_print()
{
	struct base_station *bs;
	int count = 0;

	print_timestamp();
	if (cmdline_opts.format_json)
		printf("[");

	llist_for_each_entry(bs, &base_stations, entry) {
		if (cmdline_opts.format_json) {
			if (count)
				printf(",");
			printf("\n%s", bs->line);
		} else {
			printf("%3d: %s\n", count, bs->line);
		}
		count++;
	}

	if (cmdline_opts.format_json)
		printf("%c]\n", count ? '\n': ' ');

	printf("\nTotal: %d\n", count);
}

static void base_stations_bump(bool known_changed)
{
	bool changed = known_changed;
	if (base_stations_timeout())
		changed = true;

	if (changed)
		base_stations_print();
}

static void handle_response(unsigned char *buf, int len)
{
	static unsigned int responses = 0;
	responses++;

	if (cmdline_opts.list_view) {
		bool changed = false;
		struct base_station *bs = base_station_parse(buf, len);
		if (base_stations_add(bs))
			changed = true;
		else
			talloc_free(bs);
		base_stations_bump(changed);
		printf("RX: %u   \r", responses);
	} else {
		printf("%s\n", parse_response(ctx, buf, len));
	}
	fflush(stdout);
}

static int read_response(int fd)
{
	unsigned char buf[255];
	struct sockaddr_in sa;
	int len;
	socklen_t sa_len = sizeof(sa);

	len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&sa, &sa_len);
	if (len < 0)
		return len;

	/* 2 bytes length, 1 byte protocol */
	if (buf[2] != IPAC_PROTO_IPACCESS)
		return 0;

	if (buf[4] != IPAC_MSGT_ID_RESP)
		return 0;

	handle_response(buf+6, len-6);
	return 0;
}

static int bfd_cb(struct osmo_fd *bfd, unsigned int flags)
{
	if (flags & BSC_FD_READ)
		return read_response(bfd->fd);
	if (flags & BSC_FD_WRITE) {
		bfd->when &= ~BSC_FD_WRITE;
		return bcast_find(bfd->fd);
	}
	return 0;
}

static struct osmo_timer_list timer;

static void timer_cb(void *_data)
{
	struct osmo_fd *bfd = _data;

	bfd->when |= BSC_FD_WRITE;

	base_stations_bump(false);

	osmo_timer_schedule(&timer, cmdline_opts.send_interval, 0);
}

int main(int argc, char **argv)
{
	struct osmo_fd bfd;
	int rc;

	printf("abisip-find (C) 2009-2010 by Harald Welte\n");
	printf("            (C) 2017 by sysmocom - s.f.m.c. GmbH\n");
	printf("This is FREE SOFTWARE with ABSOLUTELY NO WARRANTY\n\n");

	handle_options(argc, argv);

	if (!cmdline_opts.ifname && !cmdline_opts.bind_ip)
		fprintf(stdout, "- You might need to specify the outgoing network interface,\n"
			"  e.g. ``%s eth0'' (requires root permissions),\n"
			"  or alternatively use -b to bind to the source address\n"
			"  assigned to that interface\n", argv[0]);
	if (!cmdline_opts.list_view)
		fprintf(stdout, "- You may find the --list-view option convenient.\n");
	else if (cmdline_opts.send_interval >= cmdline_opts.list_view_timeout)
		fprintf(stdout, "\nWARNING: the --timeout should be larger than --interval.\n\n");

	bfd.cb = bfd_cb;
	bfd.when = BSC_FD_READ | BSC_FD_WRITE;
	bfd.fd = udp_sock(cmdline_opts.ifname, cmdline_opts.bind_ip);
	if (bfd.fd < 0) {
		perror("Cannot create local socket for broadcast udp");
		exit(1);
	}

	rc = osmo_fd_register(&bfd);
	if (rc < 0) {
		fprintf(stderr, "Cannot register FD\n");
		exit(1);
	}

	osmo_timer_setup(&timer, timer_cb, &bfd);
	osmo_timer_schedule(&timer, cmdline_opts.send_interval, 0);

	printf("Trying to find ip.access BTS by broadcast UDP...\n");

	while (1) {
		rc = osmo_select_main(0);
		if (rc < 0)
			exit(3);
	}

	exit(0);
}

