/* $Id: miniupnpc-async.c,v 1.19 2014/11/07 12:05:40 nanard Exp $ */
/* miniupnpc-async
 * Copyright (c) 2008-2017, Thomas BERNARD <miniupnp@free.fr>
 * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <stdio.h>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <io.h>
#define PRINT_SOCKET_ERROR printf
#define SOCKET_ERROR GetWSALastError()
#define WOULDBLOCK(err) (err == WSAEWOULDBLOCK)
#else
#include <unistd.h>
#include <errno.h>
#define closesocket close
#define PRINT_SOCKET_ERROR perror
#define SOCKET_ERROR errno
#define WOULDBLOCK(err) (err == EAGAIN || err == EWOULDBLOCK)
#endif
#include "miniupnpc-async.h"
#include "parsessdpreply.h"
#include "upnputils.h"
#include "minixml.h"
#include "igd_desc_parse.h"
#include "upnpreplyparse.h"

#ifndef MIN
#define MIN(x,y) (((x)<(y))?(x):(y))
#endif /* MIN */

#ifndef MAXHOSTNAMELEN
#define MAXHOSTNAMELEN 64
#endif /* MAXHOSTNAMELEN */

#define SSDP_PORT 1900
#define SSDP_MCAST_ADDR "239.255.255.250"
#define XSTR(s) STR(s)
#define STR(s) #s

#ifdef DEBUG
#define debug_printf(...) fprintf(stderr, __VA_ARGS__)
#else
#define debug_printf(...)
#endif

/* stuctures */

struct upnp_args {
	const char * elt;
	const char * val;
};

/* private functions */

static int upnpc_connect(upnpc_device_t * p, const char * url);
static int upnpc_send_request(upnpc_device_t * p);


/* parse_msearch_reply()
 * the last 4 arguments are filled during the parsing :
 *    - location/locationsize : "location:" field of the SSDP reply packet
 *    - st/stsize : "st:" field of the SSDP reply packet.
 * The strings are NOT null terminated */
static void
parse_msearch_reply(const char * reply, int size,
                    const char * * location, unsigned int * locationsize,
                    const char * * st, unsigned int * stsize)
{
	int a, b, i;
	i = 0;	/* current character index */
	a = i;	/* start of the line */
	b = 0;	/* end of the "header" (position of the colon) */
	while(i<size) {
		switch(reply[i]) {
		case ':':
			if(b==0) {
				b = i; /* end of the "header" */
			}
			break;
		case '\x0a':
		case '\x0d':
			if(b!=0) {
				/* skip the colon and white spaces */
				do { b++; } while(reply[b]==' ' && b<size);
				if(0==strncasecmp(reply+a, "location", 8)) {
					*location = reply+b;
					*locationsize = i-b;
				} else if(0==strncasecmp(reply+a, "st", 2)) {
					*st = reply+b;
					*stsize = i-b;
				}
				b = 0;
			}
			a = i+1;
			break;
		default:
			break;
		}
		i++;
	}
}

static int upnpc_send_ssdp_msearch(upnpc_t * p, const char * device, unsigned int mx)
{
	/* envoyer les packets de M-SEARCH discovery sur le socket ssdp */
	int n;
	char bufr[1024];
	struct sockaddr_in addr;
	static const char MSearchMsgFmt[] = 
	"M-SEARCH * HTTP/1.1\r\n"
	"HOST: " SSDP_MCAST_ADDR ":" XSTR(SSDP_PORT) "\r\n"
	"ST: %s\r\n"
	"MAN: \"ssdp:discover\"\r\n"
	"MX: %u\r\n"
	"\r\n";

	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
    addr.sin_port = htons(SSDP_PORT);
    addr.sin_addr.s_addr = inet_addr(SSDP_MCAST_ADDR);
	n = snprintf(bufr, sizeof(bufr),
	             MSearchMsgFmt, device, mx);
	debug_printf("upnpc_send_ssdp_msearch: %s", bufr);
	n = sendto(p->ssdp_socket, bufr, n, 0,
	           (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
	if (n < 0) {
		int err = SOCKET_ERROR;
		if(err == EINTR || WOULDBLOCK(err)) {
			debug_printf("upnpc_send_ssdp_msearch: should try again");
			p->state = EUPnPSendSSDP;
			return 0;
		}
		PRINT_SOCKET_ERROR("sendto");
		return -1;
	}
	p->state = EUPnPReceiveSSDP;
	return 0;
}

static int upnpc_set_root_desc_location(upnpc_device_t * d, const char * location, int locationsize)
{
	char * tmp;
	tmp = realloc(d->root_desc_location, locationsize + 1);
	if(tmp == 0) {
		return -1;
	}
	memcpy(tmp, location, locationsize);
	tmp[locationsize] = '\0';
	d->root_desc_location = tmp;
	return 0;
}

static int upnpc_receive_and_parse_ssdp(upnpc_t * p)
{
	int n;
	char bufr[1024];
	n = recv(p->ssdp_socket, bufr, sizeof(bufr), 0);
	if (n<0) {
		PRINT_SOCKET_ERROR("recv");
	} else if (n==0) {
		debug_printf("empty packet received\n");
	} else {
		const char * location = NULL;
		unsigned int locationsize;
		const char * st = NULL;
		unsigned int stsize;
		debug_printf("%.*s", n, bufr);
		parse_msearch_reply(bufr, n, &location, &locationsize, &st, &stsize);
		debug_printf("location = '%.*s'\n", locationsize, location);
		debug_printf("st = '%.*s'\n", stsize, st);
		if(location != NULL) {
			upnpc_device_t * dev = p->device_list;
			while(dev != NULL) {
				if(dev->root_desc_location != NULL
				   && strlen(dev->root_desc_location) == locationsize
			       && memcmp(dev->root_desc_location, location, locationsize) == 0) {
					debug_printf("device already in list (location='%s')\n", dev->root_desc_location);
					return -1;
				}
				dev = dev->next;
			}
			dev = calloc(1, sizeof(upnpc_device_t));
			if(dev == NULL) {
				p->state = EUPnPError;
				return -1;
			}
			if(upnpc_set_root_desc_location(dev, location, locationsize) < 0) {
				free(dev);
				p->state = EUPnPError;
				return -1;
			}
			dev->next = p->device_list;
			p->device_list = dev;
			dev->state = EDevGetDescConnect;
			upnpc_connect(dev, dev->root_desc_location);
		} else {
			/* or do nothing ? */
			p->state = EUPnPError;
		}
	}
	return 0;
}

static int
parseURL(const char * url,
         char * hostname, unsigned short * port,
         char * * path, unsigned int * scope_id)
{
	char * p1, *p2, *p3;
	if(!url)
		return 0;
	p1 = strstr(url, "://");
	if(!p1)
		return 0;
	p1 += 3;
	if(  (url[0]!='h') || (url[1]!='t')
	   ||(url[2]!='t') || (url[3]!='p'))
		return 0;
	memset(hostname, 0, MAXHOSTNAMELEN + 1);
	if(*p1 == '[') {
		/* IP v6 : http://[2a00:1450:8002::6a]/path/abc */
		char * scope;
		scope = strchr(p1, '%');
		p2 = strchr(p1, ']');
		if(p2 && scope && scope < p2 && scope_id) {
			/* parse scope */
#ifdef IF_NAMESIZE
			char tmp[IF_NAMESIZE];
			int l;
			scope++;
			/* "%25" is just '%' in URL encoding */
			if(scope[0] == '2' && scope[1] == '5')
				scope += 2;	/* skip "25" */
			l = p2 - scope;
			if(l >= IF_NAMESIZE)
				l = IF_NAMESIZE - 1;
			memcpy(tmp, scope, l);
			tmp[l] = '\0';
			*scope_id = if_nametoindex(tmp);
			if(*scope_id == 0) {
				*scope_id = (unsigned int)strtoul(tmp, NULL, 10);
			}
#else
			/* under windows, scope is numerical */
			char tmp[8];
			int l;
			scope++;
			/* "%25" is just '%' in URL encoding */
			if(scope[0] == '2' && scope[1] == '5')
				scope += 2;	/* skip "25" */
			l = p2 - scope;
			if(l >= (int)sizeof(tmp))
				l = sizeof(tmp) - 1;
			memcpy(tmp, scope, l);
			tmp[l] = '\0';
			*scope_id = (unsigned int)strtoul(tmp, NULL, 10);
#endif
		}
		p3 = strchr(p1, '/');
		if(p2 && p3) {
			p2++;
			strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1)));
			if(*p2 == ':') {
				*port = 0;
				p2++;
				while( (*p2 >= '0') && (*p2 <= '9')) {
					*port *= 10;
					*port += (unsigned short)(*p2 - '0');
					p2++;
				}
			} else {
				*port = 80;
			}
			*path = p3;
			return 1;
		}
	}
	p2 = strchr(p1, ':');
	p3 = strchr(p1, '/');
	if(!p3)
		return 0;
	if(!p2 || (p2>p3)) {
		strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p3-p1)));
		*port = 80;
	} else {
		strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1)));
		*port = 0;
		p2++;
		while( (*p2 >= '0') && (*p2 <= '9')) {
			*port *= 10;
			*port += (unsigned short)(*p2 - '0');
			p2++;
		}
	}
	*path = p3;
	return 1;
}

static int upnpc_connect(upnpc_device_t * p, const char * url)
{
	int r;
	char hostname[MAXHOSTNAMELEN+1];
	unsigned short port;
	char * path;
	unsigned int scope_id;
	struct sockaddr_in addr;
	socklen_t addrlen;

	/*if(p->root_desc_location == 0) {
		p->state = EError;
		return -1;
	}*/
	if(!parseURL(url/*p->root_desc_location*/, hostname, &port,
	             &path, &scope_id)) {
		p->state = EDevError;
		return -1;
	}
	p->http_socket = socket(PF_INET, SOCK_STREAM, 0);
	if(p->http_socket < 0) {
		PRINT_SOCKET_ERROR("socket");
		p->state = EDevError;
		return -1;
	}
	if(!set_non_blocking(p->http_socket)) {
		/* TODO : ERROR */
	}
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	inet_pton(AF_INET, hostname, &(addr.sin_addr));
	addr.sin_port = htons(port);
	addrlen = sizeof(struct sockaddr_in);
	do {
		r = connect(p->http_socket, (struct sockaddr *)&addr, addrlen);
		if(r < 0) {
			if(errno == EINPROGRESS) {
				/*p->state = EGetDescConnect;*/
				return 0;
			} else if(errno != EINTR) {
				PRINT_SOCKET_ERROR("connect");
				p->state = EDevError;
				return -1;
			}
		}
	} while(r < 0 && errno == EINTR);
	if(p->state == EDevGetDescConnect) {
		p->state = EDevGetDescRequest;
	} else {
		p->state = EDevSoapRequest;
	}
	upnpc_send_request(p);
	return 0;
}

static int upnpc_complete_connect(upnpc_device_t * p)
{
	socklen_t len;
	int err;
	len = sizeof(err);
	if(getsockopt(p->http_socket, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
		PRINT_SOCKET_ERROR("getsockopt");
		p->state = EDevError;
		return -1;
	}
	if(err != 0) {
		debug_printf("connect failed %d\n", err);
		p->state = EDevError;
		return -1;
	}
	if(p->state == EDevGetDescConnect)
		p->state = EDevGetDescRequest;
	else
		p->state = EDevSoapRequest;
	upnpc_send_request(p);
	return 0;
}

static int upnpc_send_request(upnpc_device_t * p)
{
	ssize_t n;
	static const char reqfmt[] = "GET %s HTTP/1.1\r\n"
		"Host: %s:%hu\r\n"
		"Connection: Close\r\n"
		"User-Agent: MiniUPnPc-async\r\n"
		"\r\n";

	/* retrieve "our" IP address used to connect to the UPnP device */
	p->selfaddrlen = sizeof(struct sockaddr_storage);
	if(getsockname(p->http_socket, (struct sockaddr *)&p->selfaddr, &p->selfaddrlen) < 0) {
		PRINT_SOCKET_ERROR("getsockname()");
	}

	if(p->http_request == NULL) {
		char hostname[MAXHOSTNAMELEN+1];
		unsigned short port;
		char * path;
		unsigned int scope_id;
		int len;
		if(!parseURL(p->root_desc_location, hostname, &port,
	    	         &path, &scope_id)) {
			p->state = EDevError;
			return -1;
		}
		len = snprintf(NULL, 0, reqfmt, path, hostname, port);
		p->http_request = malloc(len + 1);
		if(p->http_request == NULL) {
			p->state = EDevError;
			return -1;
		}
		p->http_request_len = snprintf(p->http_request, len + 1,
		                               reqfmt, path, hostname, port);
		p->http_request_sent = 0;
	}
	n = send(p->http_socket, p->http_request + p->http_request_sent,
	         p->http_request_len - p->http_request_sent, 0/* flags */);
	if(n < 0) {
		PRINT_SOCKET_ERROR("send");
		p->state = EDevError;
		return -1;
	} else {
		debug_printf("sent %d bytes\n", (int)n);
		/*if(n == 0) {
			p->state = EError;
			return -1;
		}*/
		p->http_request_sent += n;
		if(p->http_request_sent >= p->http_request_len) {
			/* all bytes sent */
#if 0
			shutdown(p->http_socket, SHUT_WR);	/* some routers don't like that */
#endif
			free(p->http_request);
			p->http_request = NULL;
			p->http_request_len = 0;
			if(p->state == EDevGetDescRequest)
				p->state = EDevGetDescResponse;
			else
				p->state = EDevSoapResponse;
			free(p->http_response);
			p->http_response = NULL;
			p->http_response_received = 0;
			p->http_response_end_of_headers = 0;
			/* get response */
		}
	}
	return 0;
}

static int upnpc_parse_headers(upnpc_device_t * p)
{
	/* search for CR LF CR LF (end of headers)
	 * recognize also LF LF */
	int i = 0;
	while(i < (p->http_response_received-1) &&
	      p->http_response_end_of_headers == 0) {
		if(p->http_response[i] == '\r') {
			i++;
			if(p->http_response[i] == '\n') {
				i++;
				if(i < p->http_response_received && p->http_response[i] == '\r') {
					i++;
					if(i < p->http_response_received && p->http_response[i] == '\n') {
						p->http_response_end_of_headers = i + 1;
					}
				}
			}
		} else if(p->http_response[i] == '\n') {
			i++;
			if(p->http_response[i] == '\n') {
				p->http_response_end_of_headers = i + 1;
			}
		}
		i++;
	}
	if(p->http_response_end_of_headers != 0) {
		int colon = 0;
		int linestart = 0;
		int valuestart = 0;
		p->http_response_code = -1;
		for(i = 0; i < p->http_response_end_of_headers - 1; i++) {
			if(linestart == 0) {
				/* reading HTTP response code on the 1st line */
				if(p->http_response[i] == ' ' && p->http_response_code < 0)
					p->http_response_code = 0;
				else if(p->http_response[i] >= '0' && p->http_response[i] <= '9') {
					p->http_response_code = p->http_response_code * 10 + (p->http_response[i] - '0');
				} else if(p->http_response[i] == ' ')
					linestart = 1;
			}
			if(colon <= linestart && p->http_response[i] == ':') {
				colon = i;
				while(i < p->http_response_end_of_headers - 1 &&
				      (p->http_response[i+1] == ' ' || p->http_response[i+1] == '\t'))
					i++;
				valuestart = i + 1;
			} else if(p->http_response[i + 1] == '\r' ||
			          p->http_response[i + 1] == '\n') {
				if(colon > linestart && valuestart > colon) {
					debug_printf("header='%.*s', value='%.*s'\n",
					       colon-linestart, p->http_response+linestart,
					       i+1-valuestart, p->http_response+valuestart);
					if(0==strncasecmp(p->http_response+linestart, "content-length", colon-linestart)) {
						p->http_response_content_length = atoi(p->http_response + valuestart);
						debug_printf("Content-Length: %d\n", p->http_response_content_length);
						if(p->http_response_content_length < 0) {
							debug_printf("Content-Length overflow ? setting to 0\n");
							p->http_response_content_length = 0;
						}
					} else if(0==strncasecmp(p->http_response+linestart, "transfer-encoding", colon-linestart)
						   && 0==strncasecmp(p->http_response+valuestart, "chunked", 7)) {
						debug_printf("Chunked transfer-encoding !\n");
						p->http_response_chunked = 1;
					}
				}
				/* find next line */
				while((i < p->http_response_received) &&
				      (p->http_response[i]=='\r' || p->http_response[i] == '\n'))
					i++;
				linestart = i;
				colon = linestart;
				valuestart = 0;
			}
		}
	}
	return 0;
}

static char * build_url_string(const char * urlbase, const char * root_desc_url, const char * controlurl)
{
	int l, n;
	char * s;
	const char * base;
	char * p;
	/* if controlurl is an absolute url, return it */
	if(0 == memcmp("http://", controlurl, 7))
		return strdup(controlurl);
	base = (urlbase[0] == '\0') ? root_desc_url : urlbase;
	n = strlen(base);
	if(n > 7) {
		p = strchr(base + 7, '/');
		if(p)
			n = p - base;
	}
	l = n + strlen(controlurl) + 1;
	if(controlurl[0] != '/')
		l++;
	s = malloc(l);
	if(s == NULL) return NULL;
	memcpy(s, base, n);
	if(controlurl[0] != '/')
		s[n++] = '/';
	memcpy(s + n, controlurl, l - n);
	return s;
}

static int upnpc_get_response(upnpc_device_t * p)
{
	ssize_t n;
	ssize_t count;
	char buffer[2048];
	if(p->http_response_content_length > 0) {
		count = p->http_response_content_length
		      + p->http_response_end_of_headers
		      - p->http_response_received;
		if(count > (ssize_t)sizeof(buffer)) count = sizeof(buffer);
	} else {
		count = sizeof(buffer);
	}
	debug_printf("recv(..., %d)\n", (int)count);
	n = recv(p->http_socket, buffer, count, 0/* flags */);
	if(n < 0) {
		if(errno == EINTR || WOULDBLOCK(errno))
			return 0;	/* try again later */
		PRINT_SOCKET_ERROR("read");
		p->state = EDevError;
		return -1;
	} else if(n == 0) {
		/* receiving finished */
		debug_printf("%.*s\n", p->http_response_received, p->http_response);
		close(p->http_socket);
		p->http_socket = -1;
		/* parse */
		if(p->http_response_end_of_headers == 0) {
			upnpc_parse_headers(p);
		}
		/* TODO : decode chunked transfer-encoding */
		/* parse xml */
		if(p->state == EDevGetDescResponse) {
			struct IGDdatas igd;
			struct xmlparser parser;
			memset(&igd, 0, sizeof(struct IGDdatas));
			memset(&parser, 0, sizeof(struct xmlparser));
			parser.xmlstart = p->http_response + p->http_response_end_of_headers;
			parser.xmlsize = p->http_response_received - p->http_response_end_of_headers;
			parser.data = &igd;
			parser.starteltfunc = IGDstartelt;
			parser.endeltfunc = IGDendelt;
			parser.datafunc = IGDdata;
			parsexml(&parser);
#ifdef DEBUG
			printIGD(&igd);
#endif /* DEBUG */
			p->control_conn_url = build_url_string(igd.urlbase, p->root_desc_location, igd.first.controlurl);
			p->control_cif_url = build_url_string(igd.urlbase, p->root_desc_location, igd.CIF.controlurl);
			debug_printf("control_conn_url='%s'\n", p->control_conn_url);
			debug_printf("control_cif_url='%s'\n", p->control_cif_url);
		} else {
			ClearNameValueList(&p->soap_response_data);
			ParseNameValue(p->http_response + p->http_response_end_of_headers,
			               p->http_response_received - p->http_response_end_of_headers,
			               &p->soap_response_data);
		}
		free(p->http_response);
		p->http_response = NULL;
		p->http_response_received = 0;
		p->http_response_end_of_headers = 0;
		p->state = EDevReady;
	} else {
		/* receiving in progress */
		debug_printf("received %d bytes:\n%.*s\n", (int)n, (int)n, buffer);
		if(p->http_response == NULL) {
			p->http_response = malloc(n);
			if(p->http_response == NULL) {
				debug_printf("failed to malloc %d bytes\n", (int)n);
				p->state = EDevError;
				return -1;
			}
			p->http_response_received = n;
			memcpy(p->http_response, buffer, n);
		} else {
			char * tmp = realloc(p->http_response, p->http_response_received + n);
			if(tmp == NULL) {
				debug_printf("failed to realloc %d bytes\n", (int)(p->http_response_received + n));
				p->state = EDevError;
				return -1;
			}
			p->http_response = tmp;
			memcpy(p->http_response + p->http_response_received, buffer, n);
			p->http_response_received += n;
		}
		if(p->http_response_end_of_headers == 0) {
			upnpc_parse_headers(p);
		}
	}
	return 0;
}

#define SOAPPREFIX "s"
#define SERVICEPREFIX "u"
#define SERVICEPREFIX2 'u'

static int upnpc_build_soap_request(upnpc_device_t * p, const char * url,
                                    const char * service,
                                    const char * action,
                                    const struct upnp_args * args, int arg_count)
{
	char * body;
	const char fmt_soap[] = 
		"<?xml version=\"1.0\"?>\r\n"
		"<" SOAPPREFIX ":Envelope "
		"xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" "
		SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
		"<" SOAPPREFIX ":Body>"
		"<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">"
		"%s"
		"</" SERVICEPREFIX ":%s>"
		"</" SOAPPREFIX ":Body></" SOAPPREFIX ":Envelope>"
		"\r\n";
	int body_len;
	const char fmt_http[] =
		"POST %s HTTP/1.1\r\n"
		"Host: %s%s\r\n"
		"User-Agent: MiniUPnPc-async\r\n"
		"Content-Length: %d\r\n"
		"Content-Type: text/xml\r\n"
		"SOAPAction: \"%s#%s\"\r\n"
		"Connection: Close\r\n"
		"Cache-Control: no-cache\r\n"	/* ??? */
		"Pragma: no-cache\r\n"
		"\r\n"
		"%s";
	char hostname[MAXHOSTNAMELEN+1];
	unsigned short port;
	char * path;
	unsigned int scope_id;
	char portstr[8];
	char * args_xml = NULL;

	if(arg_count > 0) {
		int i;
		size_t l, n;
		for(i = 0, l = 0; i < arg_count; i++) {
			/* <ELT>VAL</ELT> */
			l += strlen(args[i].elt) * 2 + strlen(args[i].val) + 5;
		}
		args_xml = malloc(++l);
		if(args_xml == NULL) {
			p->state = EDevError;
			return -1;
		}
		for(i = 0, n = 0; i < arg_count && n < l; i++) {
			/* <ELT>VAL</ELT> */
			n += snprintf(args_xml + n, l - n, "<%s>%s</%s>",
			              args[i].elt, args[i].val, args[i].elt);
		}
	}

	body_len = snprintf(NULL, 0, fmt_soap, action, service, args_xml?args_xml:"", action);
	body = malloc(body_len + 1);
	if(body == NULL) {
		p->state = EDevError;
		free(args_xml);
		return -1;
	}
	if(snprintf(body, body_len + 1, fmt_soap, action, service, args_xml?args_xml:"", action) != body_len) {
		debug_printf("snprintf() returned strange value...\n");
	}
	free(args_xml);
	args_xml = NULL;
	if(!parseURL(url, hostname, &port, &path, &scope_id)) {
		p->state = EDevError;
		free(body);
		return -1;
	}
	if(port != 80)
		snprintf(portstr, sizeof(portstr), ":%hu", port);
	else
		portstr[0] = '\0';
	p->http_request_len = snprintf(NULL, 0, fmt_http,
	                               path/*url*/, hostname, portstr, body_len, service, action, body);
	free(p->http_request);
	p->http_request = malloc(p->http_request_len + 1);
	if(snprintf(p->http_request, p->http_request_len + 1, fmt_http,
	            path/*url*/, hostname, portstr, body_len, service, action, body) != p->http_request_len) {
		debug_printf("snprintf() returned strange value...\n");
	}
	free(body);
	debug_printf("%s", p->http_request);
	p->http_request_sent = 0;
	return 0;
}

/* public functions */
int upnpc_init(upnpc_t * p, const char * multicastif)
{
	int opt = 1;
	struct sockaddr_in addr;
	if(!p)
		return UPNPC_ERR_INVALID_ARGS;
	p->state = EUPnPError;
	memset(p, 0, sizeof(upnpc_t)); /* clean everything */
	/* open the socket for SSDP */
	p->ssdp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if(p->ssdp_socket < 0) {
		return UPNPC_ERR_SOCKET_FAILED;
	}
	/* set REUSEADDR */
#ifdef WIN32
	if(setsockopt(p->ssdp_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt)) < 0) {
#else
	if(setsockopt(p->ssdp_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
#endif
		/* non fatal error ! */
	}
	if(!set_non_blocking(p->ssdp_socket)) {
		/* TODO log error */
	}

	/* receive address */
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = INADDR_ANY;
	/*addr.sin_port = htons(SSDP_PORT);*/

	if(multicastif) {
		struct in_addr mc_if;
		mc_if.s_addr = inet_addr(multicastif);
    	addr.sin_addr.s_addr = mc_if.s_addr;
		if(setsockopt(p->ssdp_socket, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&mc_if, sizeof(mc_if)) < 0) {
			PRINT_SOCKET_ERROR("setsockopt");
			/* non fatal error ! */
		}
	}

	/* bind the socket to the ssdp address in order to receive responses */
	if(bind(p->ssdp_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) != 0) {
		close(p->ssdp_socket);
		return UPNPC_ERR_BIND_FAILED;
	}

	p->state = EUPnPInit;
	return UPNPC_OK;
}

int upnpc_finalize(upnpc_t * p)
{
	if(!p) return UPNPC_ERR_INVALID_ARGS;
	if(p->ssdp_socket >= 0) {
		close(p->ssdp_socket);
		p->ssdp_socket = -1;
	}
	while(p->device_list) {
		upnpc_device_t * next = p->device_list->next;
		free(p->device_list->root_desc_location);
		p->device_list->root_desc_location = NULL;
		free(p->device_list->http_request);
		p->device_list->http_request = NULL;
		free(p->device_list->http_response);
		p->device_list->http_response = NULL;
		free(p->device_list->control_cif_url);
		p->device_list->control_cif_url = NULL;
		free(p->device_list->control_conn_url);
		p->device_list->control_conn_url = NULL;
		if(p->device_list->http_socket >= 0) {
			close(p->device_list->http_socket);
			p->device_list->http_socket = -1;
		}
		ClearNameValueList(&p->device_list->soap_response_data);
		free(p->device_list);
		p->device_list = next;
	}
	p->state = EUPnPFinalized;
	return UPNPC_OK;
}

int upnpc_get_external_ip_address(upnpc_device_t * p)
{
	upnpc_build_soap_request(p, p->control_conn_url,
	                         "urn:schemas-upnp-org:service:WANIPConnection:1",
	                         "GetExternalIPAddress", NULL, 0);
	p->state = EDevSoapConnect;
	upnpc_connect(p, p->control_conn_url);
	return 0;
}

int upnpc_get_link_layer_max_rate(upnpc_device_t * p)
{
	upnpc_build_soap_request(p, p->control_cif_url,
	                         "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
	                         "GetCommonLinkProperties", NULL, 0);
	p->state = EDevSoapConnect;
	upnpc_connect(p, p->control_conn_url);
	return 0;
}

int upnpc_add_port_mapping(upnpc_device_t * p,
                           const char * remote_host, unsigned short ext_port,
                           unsigned short int_port, const char * int_client,
                           const char * proto, const char * description,
                           unsigned int lease_duration)
{
	struct upnp_args args[8];
	char lease_duration_str[16];
	char int_port_str[8];
	char ext_port_str[8];

	if(int_client == NULL || int_port == 0 || ext_port == 0 || proto == NULL)
		return UPNPC_ERR_INVALID_ARGS;
	snprintf(lease_duration_str, sizeof(lease_duration_str), "%u", lease_duration);
	snprintf(int_port_str, sizeof(int_port_str), "%hu", int_port);
	snprintf(ext_port_str, sizeof(ext_port_str), "%hu", ext_port);
	args[0].elt = "NewRemoteHost";
	args[0].val = remote_host?remote_host:"";
	args[1].elt = "NewExternalPort";
	args[1].val = ext_port_str;
	args[2].elt = "NewProtocol";
	args[2].val = proto;
	args[3].elt = "NewInternalPort";
	args[3].val = int_port_str;
	args[4].elt = "NewInternalClient";
	args[4].val = int_client;
	args[5].elt = "NewEnabled";
	args[5].val = "1";
	args[6].elt = "NewPortMappingDescription";
	args[6].val = description?description:"miniupnpc-async";
	args[7].elt = "NewLeaseDuration";
	args[7].val = lease_duration_str;
	upnpc_build_soap_request(p, p->control_conn_url,
	                         "urn:schemas-upnp-org:service:WANIPConnection:1",
	                         "AddPortMapping",
	                         args, 8);
	p->state = EDevSoapConnect;
	upnpc_connect(p, p->control_conn_url);
	return 0;
}

#ifdef UPNPC_USE_SELECT
int upnpc_select_fds(upnpc_t * p, int * nfds, fd_set * readfds, fd_set * writefds)
{
	upnpc_device_t * d;
	int n = 0;
	if(!p) return UPNPC_ERR_INVALID_ARGS;
	for(d = p->device_list; d != NULL; d = d->next) {
		switch(d->state) {
		case EDevGetDescConnect:
		case EDevGetDescRequest:
		case EDevSoapConnect:
		case EDevSoapRequest:
			FD_SET(d->http_socket, writefds);
			if(*nfds < d->http_socket)
				*nfds = d->http_socket;
			n++;
			break;
		case EDevGetDescResponse:
		case EDevSoapResponse:
			FD_SET(d->http_socket, readfds);
			if(*nfds < d->http_socket)
				*nfds = d->http_socket;
			n++;
			break;
		default:
			break;
		}
	}

	switch(p->state) {
	case EUPnPSendSSDP:
		FD_SET(p->ssdp_socket, writefds);
		if(*nfds < p->ssdp_socket)
			*nfds = p->ssdp_socket;
		n++;
		break;
	case EUPnPReceiveSSDP:
	default:
		/* still receive SSDP responses when processing Description, etc. */
		FD_SET(p->ssdp_socket, readfds);
		if(*nfds < p->ssdp_socket)
			*nfds = p->ssdp_socket;
		n++;
		break;
	}
	return n;
}

void upnpc_check_select_fds(upnpc_t * p, const fd_set * readfds, const fd_set * writefds)
{
	upnpc_device_t * d;

	p->socket_flags = 0;
	if(FD_ISSET(p->ssdp_socket, readfds))
		p->socket_flags = UPNPC_SSDP_READABLE;
	if(FD_ISSET(p->ssdp_socket, writefds))
		p->socket_flags = UPNPC_SSDP_WRITEABLE;

	for(d = p->device_list; d != NULL; d = d->next) {
		d->socket_flags = 0;
		if(FD_ISSET(d->http_socket, readfds))
			d->socket_flags = UPNPC_HTTP_READABLE;
		if(FD_ISSET(d->http_socket, writefds))
			d->socket_flags = UPNPC_HTTP_WRITEABLE;
	}
}
#endif

static const char * devices_to_search[] = {
	"urn:schemas-upnp-org:device:InternetGatewayDevice:1",
	"urn:schemas-upnp-org:service:WANIPConnection:1",
	"urn:schemas-upnp-org:service:WANPPPConnection:1",
	"upnp:rootdevice",
	0
};

int upnpc_process(upnpc_t * p)
{
	upnpc_device_t * d;
/*
1)	Envoyer les paquets de discovery SSDP
2)	Recevoir et traiter les reponses
3)	recup les descriptions
4)	tester les etats
*/
	if(!p) return UPNPC_ERR_INVALID_ARGS;
	debug_printf("state=%d   socket_flags=0x%04x\n", (int)p->state, p->socket_flags);

	for(d = p->device_list; d != NULL; d = d->next) {
		switch(d->state) {
		case EDevGetDescConnect:
		case EDevSoapConnect:
			upnpc_complete_connect(d);
			break;
		case EDevGetDescRequest:
		case EDevSoapRequest:
			upnpc_send_request(d);
			break;
		case EDevGetDescResponse:
		case EDevSoapResponse:
			upnpc_get_response(d);
			break;
		default:
			break;
		}
	}
	/* all devices ready => ready */
	if(p->device_list != NULL) {
		d = p->device_list;
		while(d && d->state == EDevReady) d = d->next;
		p->state = (d == NULL) ? EUPnPReady : EUPnPProcessing;
	}

	if(p->socket_flags & UPNPC_SSDP_READABLE) {
		upnpc_receive_and_parse_ssdp(p);
	}
	switch(p->state) {
	case EUPnPInit:
		upnpc_send_ssdp_msearch(p, devices_to_search[0], 2);
		break;
	case EUPnPSendSSDP:
		upnpc_send_ssdp_msearch(p, devices_to_search[0], 2);
		break;
	case EUPnPReceiveSSDP:
		/*upnpc_receive_and_parse_ssdp(p);*/
		break;
	/*case EGetDesc:
		upnpc_connect(p);
		break;*/
	case EUPnPReady:
	case EUPnPProcessing:
		break;
	default:
		return UPNPC_ERR_UNKNOWN_STATE;
	}
	return UPNPC_OK;
}