/*
 * Copyright Jason Pacheco and Trevor Carlson 
 * Distributed under the GPL License
 *
 * Version 0.1, 20050518
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/mman.h>

#include <glib.h>

#define DEST_IP "239.255.255.250"
#define DEST_PORT 1900
#define NUM_TIMES 2
#define TIMEOUT 3
#define READ 0
#define WRITE 1

#define STRING_SIZE 128

struct xml_data {
	char service_type[STRING_SIZE];
	char control_url[STRING_SIZE];
	char *cur_loc;		/* state for xml_parse */
	int  in_service;	/* state for xml_parse */
	int  found_it;		/* state for xml_parse */
};

struct port_req_data {
	char req_path[STRING_SIZE];
	char req_host[16];
	char req_port[16];
	char bytes_body[16];
	char map_remote_host[16];
	char map_ext_port[16];
	char map_protocol[16];
	char map_int_port[16];
	char map_int_client[16];
	char map_desc[STRING_SIZE];
	char map_lease_time[16];
};

/* F U N C T I O N S **********************************************************/

int find_igd_loc(char *location, const char *buf)
{
	int length;
	char *found_loc, *end;
	char service[] = "schemas-upnp-org:service:WANIPConnection:1";

	/* check if we are the correct service */
	found_loc=strstr(buf,service);

	if (found_loc==NULL) {
		return(1);
	} else {
		found_loc=strstr(buf,"http://");
		if (found_loc==NULL)
			return(1);
		end = index(found_loc, '\r');
		length = end-found_loc;
		strncpy(location,found_loc,length);
		location[length] = '\0';

		fprintf(stderr, "find_igd_loc: for %s found %s.\n", service, location);
		
		return(0);
	}
}

int udp_listen(char *loc, int sockfd, struct sockaddr_in dest_addr)
{
	int fromlen = sizeof(dest_addr);
	char buf[1024] = "";

	/* listen for responses */
	for(;;) {

		/* receive from socket */
		if(recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *) &dest_addr,
				&fromlen) == -1) {
			perror("recvfrom");
			return(1);			
		}

		fprintf(stderr,"upnp_listen: Recieved %d bytes from %s:%d\n",strlen(buf), 
			inet_ntoa(dest_addr.sin_addr), ntohs(dest_addr.sin_port));
	
		/* does it match? */
		if (!find_igd_loc(loc, buf))
			return(0);
	}

	return(1);
}

int udp_search_devs(int sockfd, struct sockaddr_in dest_addr, int num_times)
{
	int i, numbytes;
	char * msg = "M-SEARCH * HTTP/1.1\r\n"
				"Host:239.255.255.250:1900\r\n"
				"ST:ssdp:all\r\n"
				"Man:\"ssdp:discover\"\r\n"
    			"MX:3\r\n"
    			"\r\n"
    			"\r\n";

	/* send request */
	for(i=0; i<num_times; i++) {
		if((numbytes = sendto(sockfd, msg, strlen(msg), 0, 
				(struct sockaddr *)&dest_addr, sizeof(dest_addr))) == -1) {
			perror("sendto");
			return 1;
		}
		fprintf(stderr,"sent %d bytes to %s:%d\n", numbytes,
				inet_ntoa(dest_addr.sin_addr), ntohs(dest_addr.sin_port));
	}

	return(0);
}

int udp_req_upnp_loc(char *loc)
{
	int rc, sockfd;
	struct sockaddr_in dest_addr;

	/* basic initialization */
	sockfd = -1;
	bzero(&dest_addr, sizeof(dest_addr));
	
	/* setup socket */
	if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
		perror("socket");
		return(1);
	}
	
	/* setup address */
	dest_addr.sin_family = AF_INET;
	dest_addr.sin_port = htons(DEST_PORT);
	dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
	bzero(&(dest_addr.sin_zero),8);
	
	/* search upnp devices */	
	rc=udp_search_devs(sockfd, dest_addr, NUM_TIMES);
	if(rc) goto out;
	
	/* get the results */
	rc=udp_listen(loc, sockfd, dest_addr);
	
out:
	/* close socket */
	if(close(sockfd) == -1) {
		perror("close");
		return(1);
	}
	return rc;
}

void xml_start_element(GMarkupParseContext *context,
				  const gchar         *element_name,
				  const gchar        **attribute_names,
				  const gchar        **attribute_values,
				  gpointer             user_data,
				  GError             **error)
{
	struct xml_data *xdata = (struct xml_data*) user_data;

	if (xdata->found_it)
		return;
	
	if (!xdata->in_service) {
		/* not in a service element yet */
		if (0==strcmp("service",element_name)) {
			xdata->in_service=1;
		}
	} else {
		/* in the service element, looking for other data */
		if (0==strcmp("serviceType",element_name)) {
			xdata->cur_loc = xdata->service_type;
		} else if (0==strcmp("controlURL",element_name)) {
			xdata->cur_loc = xdata->control_url;
		}
	}
}

void xml_end_element(GMarkupParseContext *context,
				const gchar         *element_name,
				gpointer             user_data,
				GError             **error)
{
	struct xml_data *xdata = (struct xml_data*) user_data;

	if (xdata->found_it)
		return;

	if (xdata->cur_loc) {
		/* we should be closing out here! */
		if (0!=strcmp("serviceType",element_name) &&
				0!=strcmp("controlURL",element_name)) {
			fprintf(stderr,"end_parse_error! (cur_loc)\n");
		}
		/* reset our state */
		xdata->cur_loc = NULL;
	} else if (xdata->in_service) {
		/* we could be leaving <service> */
		if (0==strcmp("service",element_name)) {
			/* reset our state */
			xdata->in_service=0;
			/* check for a match, and mark accordingly */
			if (0==strcmp("urn:schemas-upnp-org:service:WANIPConnection:1",
					xdata->service_type)) {
				xdata->found_it=1;
			}
		}
	}
}

void xml_text(GMarkupParseContext *context,
		 const gchar         *text,
		 gsize                text_len,
		 gpointer             user_data,
		 GError             **error)
{
	struct xml_data *xdata = (struct xml_data *) user_data;

	if (xdata->found_it)
		return;
	if (text_len > STRING_SIZE) {
		fprintf(stderr, "Warning, text is longer than buffer!\n");
	}

	if (xdata->in_service && xdata->cur_loc) {
		gsize length=text_len<STRING_SIZE-1?text_len:STRING_SIZE-1;
		memcpy(xdata->cur_loc,text,length);
		xdata->cur_loc[length]='\0';
	}
}

int parse_xml(char *req_path, const char *data, int len) {

	GMarkupParseContext *ctxt;
	GError *err;
	struct xml_data xdata;
	GMarkupParser parser = {
		.start_element = xml_start_element,
		.end_element = xml_end_element,
		.text = xml_text,
		.passthrough = NULL,
		.error = NULL,
	};

	/* init me */
	bzero(&xdata,sizeof(xdata));
	ctxt=g_markup_parse_context_new(&parser,0,&xdata,NULL);
	if (ctxt==NULL) return(1);
	
	/* parse me */
	g_markup_parse_context_parse(ctxt,data,len,&err);

	/* free me */
	g_markup_parse_context_free(ctxt);

	if (xdata.found_it) {
		fprintf(stderr,"parse_xml: Found it: [%s]\n",xdata.control_url);
		memcpy(req_path,xdata.control_url,STRING_SIZE);
		return(0);
	} else {
		fprintf(stderr,"parse_xml: Did not find a match!\n");
		return(1);
	}
}

int get_ctrl_url_from_loc(char *req_path, const char *location)
{
	int rc, fd;
	char *data, *filename;
	char command[256]="wget -q -N "; /* quiet/timestamps for no-overwrite */
	struct stat f_stat;
	/* we should use gaim_url_fetch, but for now, wget+mmap is our friend. */
	
	strncat(command,location,255);
	command[255]='\0';
	rc=system(command);
	if (rc!=0) return(1);
	filename=rindex(location,'/');
	filename++;
	fd=open(filename,O_RDONLY);
	if (fd==-1)	{
		perror("open");
		return(1);
	}
	rc=stat(filename,&f_stat);
	if (rc==-1) {
		perror("stat");
		return(1);
	}
	data=mmap(0,f_stat.st_size,PROT_READ,MAP_PRIVATE,fd,0);
	if (data==MAP_FAILED) {
		perror("mmap");
		return(1);
	}

	/* process the xml data */
	rc=parse_xml(req_path,data,f_stat.st_size);
	
	/* clean up */
	munmap(data,f_stat.st_size);
	close(fd);
	
	return rc;
}

int request_add_server(struct port_req_data *req_data, char *location)
{
	int len;
	char *ptr, *rc;
	rc=strstr(location,"http://");
	if (rc==NULL)
		return(1);

	/* look for the IP address */
	ptr=rc;
	ptr+=7;		/* move past the "http://" */
	rc=index(ptr,':');
	len=rc-ptr;
	if (len>(sizeof(req_data->req_host)-1)) return(1);
	memcpy(req_data->req_host,ptr,len);
	req_data->req_host[len]='\0';
	fprintf(stderr,"request_add_server: found host: [%s]\n",req_data->req_host);

	/* now looking for the port number */
	ptr=rc;
	ptr+=1;		/* move past the ";" */
	rc=index(ptr,'/');
	len=rc-ptr;
	if (len>(sizeof(req_data->req_port)-1)) return(1);
	memcpy(req_data->req_port,ptr,len);
	req_data->req_port[len]='\0';
	fprintf(stderr,"request_add_server: found port: [%s]\n",req_data->req_port);
	
	return(0);
}

char *build_header_request(struct port_req_data *req_data, int body_len,
		int need_man_header)
{
	char *new_head;
	char *req_head="POST %s HTTP/1.1\r\n"
					"HOST: %s:%s\r\n"
					"CONTENT-LENGTH: %d\r\n"
					"CONTENT-TYPE: text/xml; chatset=\"utf-8\"\r\n"
					"SOAPACTION: \"urn:schemas-upnp-org:service:"
					              "WANIPConnection:1#AddPortMapping\"\r\n"
					"\r\n";
	
	if (need_man_header)
		return NULL;

	new_head=(char *)malloc(sizeof(req_head)+sizeof(req_data));
	
	if (!new_head)
		return NULL;
	
	sprintf(new_head,req_head,req_data->req_path,req_data->req_host,
			req_data->req_port,body_len);

	fprintf(stderr,"head data (len=%d):\n%s",strlen(new_head),new_head);
	
	return new_head;
}

char *build_body_request(struct port_req_data *req_data)
{
	char *new_body;		
	char *req_body="<s:Envelope\r\n"
		"xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"\r\n"
		"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"
		"<s:Body>\r\n"
		"<u:AddPortMapping\r\n"
		"xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n"
		"<NewRemoteHost>%s</NewRemoteHost>\r\n"
		"<NewExternalPort>%s</NewExternalPort>\r\n"
		"<NewProtocol>%s</NewProtocol>\r\n"
		"<NewInternalPort>%s</NewInternalPort>\r\n"
		"<NewInternalClient>%s</NewInternalClient>\r\n"
		"<NewEnabled>1</NewEnabled>\r\n"
		"<NewPortMappingDescription>%s</NewPortMappingDescription>\r\n"
		"<NewLeaseDuration>%s</NewLeaseDuration>\r\n"
		"</u:AddPortMapping>\r\n"
		"</s:Body>\r\n"
		"<s:Envelope>\r\n"
		"\r\n";
					
	new_body=(char*)malloc(sizeof(req_body)+sizeof(struct port_req_data));
	sprintf(new_body,req_body,req_data->map_remote_host,req_data->map_ext_port,
			req_data->map_protocol,req_data->map_int_port,
			req_data->map_int_client,req_data->map_desc,
			req_data->map_lease_time);
	
	fprintf(stderr,"new_body(len=%d):\n%s",strlen(new_body),new_body);
	
	return(new_body);
}

int send_forward_request(struct port_req_data *req_data) {

	char *body, *head, *tmp, response[4096];
	int rc, sockfd;
	ssize_t left;
	struct sockaddr_in dest_addr;

	/* basic initialization */
	rc = 0;
	sockfd = -1;
	bzero(&dest_addr, sizeof(dest_addr));

	/* generate the data to send */
	body=build_body_request(req_data);
	if (!body) {
		rc=1;
		goto err;
	}
	head=build_header_request(req_data,strlen(body),0);
	if (!head) {
		rc=-1;
		goto err;
	}

	/* setup socket */
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		perror("socket");
		rc=1;
		goto err;
	}
	
	/* setup address */
	dest_addr.sin_family = AF_INET;
	dest_addr.sin_port = htons(atoi(req_data->req_port));
	dest_addr.sin_addr.s_addr = inet_addr(req_data->req_host);
	bzero(&(dest_addr.sin_zero),8);

	/* connect to the server */
	if(connect(sockfd,(struct sockaddr*)&dest_addr,sizeof(dest_addr))==-1) {
		perror("connect");
		rc=1;
		goto err_con;
	}

	/* send our buffer */
	left=strlen(head);
	tmp=head;
	while(left>0) {
		left=write(sockfd,tmp,left);
		if (left==-1) {
			perror("write");
			rc=1;
			goto err_con;
		} else {
			tmp+=left;
			left-=left;
		}
	}
	left=strlen(body);
	tmp=body;
	while(left>0) {
		left=write(sockfd,tmp,left);
		if (left==-1) {
			perror("write");
			rc=1;
			goto err_con;
		} else {
			tmp+=left;
			left-=left;
		}
	}

	/* get the response data */
	rc=read(sockfd,response,4096);
	if (rc==-1) {
		perror("read");
		rc=1;
		goto err_con;
	}
	
	fprintf(stderr,"response data:\n%s\n",response);
	
err_con:
	close(sockfd);
err:
	// FIXME, uncommenting these lines causes a seg-fault, why?
	//if (head) free(head);
	//if (body) free(body);
	return(rc);
}

/* M A I N ********************************************************************/
int main()
{
	int rc;
	char location[STRING_SIZE];
	struct port_req_data req_data={
		.req_path="",
		.req_host="",
		.req_port="",
		.bytes_body="",
		.map_remote_host="",
		.map_ext_port="65000",
		.map_protocol="UDP",
		.map_int_port="65000",
		.map_int_client="192.168.0.250",	/* Just for fun */
		/* see:
		http://lists.netisland.net/archives/plug/plug-2004-03/msg00051.html
		   for more information on determining our IP
		   nevertheless, we will most likely be supplied this by Gaim. */
		.map_desc="(test)",
		.map_lease_time="0",
	};

	/* populate the location for the http query */
	rc=udp_req_upnp_loc(location);
	if (rc) return(1);

	/* determine the control location string */
	rc=get_ctrl_url_from_loc(req_data.req_path,location);
	if (rc) return(2);

	/* grab additional server information */
	rc=request_add_server(&req_data,location);
	if (rc) return(3);

	/* send along forwarding information */
	rc=send_forward_request(&req_data);
	if (rc) return(4);
	
	return(0);
}
