/*
 * tivobeacon.c - simple Tivo Media Server beacon
 *
 * Copyright 2004 Jesse Barnes <jbarnes@virtuousgeek.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation.
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * I saw some perl based Tivo beacons out there, but my machine doesn't
 * have much memory or CPU power, so I thought a C version would be
 * better.  It's a piece of crap, but seems to work ok for me.
 *
 * Since I don't know what one would do with the knowledge of other Media
 * servers out there, this implementation ignores them.  It just blindly
 * sends its packets out and hopes someone cares about them, and doesn't
 * bother listening for any other media server packets at all.
 *
 * It should work on most Unix like systems (maybe even including cygwin)
 * but I've only tested it on Linux x86.
 *
 * It has to be run on the same machine as your media server since Tivo
 * will assume that the box generating the beacon packets has something
 * listening on the service ports specified (currently only one, but
 * that's easy to change for debugging your own media server).
 *
 * To compile it, do:
 *   $ cc -o tivobeacon tivobeacon.c
 * 
 * And then to run it:
 *   $ ./tivobeacon (it'll fork a background task unless you specify -v)
 *
 * And that's it.
 */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/utsname.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const char *name = "tivobeacon";
const char *version = "1.1";

static int verbose; /* global verbose flag */

/*
 * Allocate and return the platform name, which is the sysname plus
 * the machine type.  E.g. "pc/Linux-i686" (the specs say 'pc' is the
 * base type of non-Tivo boxes)
 *
 * On error, just return either "pc" or NULL.
 */
static char *get_platform(void)
{
	struct utsname machine_info;
	char *platform;

	if (uname(&machine_info))
		return strdup("pc");

	/* pc/<sysname>-<machine> + '\0' */
	platform = malloc(3 + strlen(machine_info.sysname) + 1 +
			  strlen(machine_info.machine) + 1);

	if (!platform)
		return strdup("pc");

	sprintf(platform, "pc/%s-%s", machine_info.sysname,
		machine_info.machine);
	return platform;
}

/*
 * Allocate and return a string with the hostname.
 *
 * Returns "(none)" or NULL on error.
 */
static char *get_machine(void)
{
	struct utsname machine_info;
	char *machine;

	if (uname(&machine_info))
		return strdup("(none)");

	/* nodename + '\0' */
	machine = malloc(strlen(machine_info.nodename) + 1);
	if (!machine)
		return strdup("(none)");

	sprintf(machine, "%s", machine_info.nodename);

	return machine;
}

/*
 * Use get_machine for now, maybe MAC address if needed?
 */
static char *get_identity(void)
{
	return strdup(get_machine());
}

/* What other services are there? */
static char *services = "TiVoMediaServer:8101/http";
static char *get_services(void)
{
	return strdup(services);
}

/*
 * tivo packet format:
 *
 * tivoconnect=<number>\n (should be 1, indicates head of packet)*
 * method=<method>\n (broadcast or connected for udp or tcp)*
 * platform=<type>[/<sub-type>]\n (should be pc unless we're on a tivo)*
 * machine=<string>\n (name of machine, may be empty)
 * identity=<string>\n (unique identifier for this machine, not ip or dns)*
 * services=<name>[:<port>][/<protocol>]\n (list of services, may be blank)
 * swversion=<string>\n (version string, may be empty)
 *
 * A '*' indicates a required field.  Broadast packets are sent via UDP
 * to port 2190.  High frequency broadcasts are considered to be once every
 * 5 seconds or so, while once a minute is considered low frequency.
 *
 * In the connected method, TCP port 2190 is opened to the target machine,
 * and a packet is sent, then the target will send its packet in return.
 *
 * When machine information has changed, high frequency broadcasting should
 * be initiated and run for about 30s.  Also, when another machine operating
 * in high frequency mode is detected, the host should immediately start
 * broadcasting in high frequency mode for a short time to allow the new
 * host to quickly discover it.
 */

static char *create_packet(void)
{
	char method[] = "broadcast";
	char *platform = get_platform();
	char *machine = get_machine();
	char *identity = get_identity();
	char *services = get_services();
	char *new_packet;
	int packet_size;

	if (!method || !platform || !machine || !identity || !services) {
		fprintf(stderr, "failed to create new packet\n");
		exit(1);
	}

	packet_size = strlen("tivoconnect=1\n") +
		strlen("method=\n") + strlen(method) +
		strlen("platform=\n") + strlen(platform) +
		strlen("machine=\n") + strlen(machine) +
		strlen("identity=\n") + strlen(identity) +
		strlen("services=\n") + strlen(services) +
		strlen("swversion=\n") + strlen(version);

	new_packet = malloc(packet_size);
	if (!new_packet)
		return NULL;

	sprintf(new_packet,
		"tivoconnect=1\n"
		"method=%s\n"
		"platform=%s\n"
		"machine=%s\n"
		"identity=%s\n"
		"services=%s\n"
		"swversion=%s\n",
		method, platform, machine, identity, services, version);

	free(platform);
	free(machine);
	free(identity);
	free(services);
	return new_packet;
}

#define TIVOBEACONPORT 2190

/*
 * Infinite broadcast loop
 */
static void broadcast_loop(char *packet)
{
	int sockfd;
	struct sockaddr_in sockaddr;
	int optval = 1, i;

	sockfd = socket(PF_INET, SOCK_DGRAM, 0);
	if (sockfd == -1) {
		perror("failed to create udp socket");
		exit(1);
	}

	if ((setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &optval,
			sizeof(optval))) == -1) {
		perror("failed to set socket broadcast option");
		exit(1);
	}

	sockaddr.sin_family = AF_INET;
	sockaddr.sin_port = htons(TIVOBEACONPORT);
	sockaddr.sin_addr.s_addr = INADDR_BROADCAST;

	if (bind(sockfd, (struct sockaddr *)&sockaddr,
		 sizeof(sockaddr)) == -1) {
		perror("failed to bind");
		exit(1);
	}

	/* High frequency broadcast for 30s */
	for (i = 0; i < 6; i++) {
		if ((sendto(sockfd, packet, strlen(packet), 0,
			    (struct sockaddr *)&sockaddr,
			    sizeof(sockaddr))) == -1)
			perror("failed to send packet\n");
		if (verbose)
			fprintf(stderr, "sent packet:\n\"%s\"\n", packet);
		sleep(5);
	}

	/* Indefinite low frequency broadcast */
	while (1) {
		if ((sendto(sockfd, packet, strlen(packet), 0,
			    (struct sockaddr *)&sockaddr,
			    sizeof(sockaddr))) == -1) {
			perror("failed to send packet");
		}
		if (verbose)
			fprintf(stderr, "sent packet:\n\"%s\"\n", packet);
		sleep(30);
	}
}

static void usage(void)
{
	printf("usage: %s [ -v ]\n", name);
	printf("	-v	verbose mode\n");
	exit(1);
}

char *optarg;
int optind, opterr, optopt;

int main(int argc, char *argv[])
{
	char *optstr = "v";
	char *packet;
	int opt;

	while ((opt = getopt(argc, argv, optstr)) != -1) {
		switch (opt) {
		case 'v':
			verbose = 1;
			break;
		default:
			usage();
		}
	}

	packet = create_packet();
	
	if (!packet) {
		fprintf(stderr, "out of memory, couldn't create packet\n");
		exit(1);
	}

	/* Fork a background task unless we're in verbose mode */
	if (verbose)
		broadcast_loop(packet);
	else if(!fork())
		broadcast_loop(packet);

	free(packet);
	return 0;
}
