/* $Id: minissdp.c,v 1.14 2008/10/06 23:08:39 nanard Exp $ */
/* MiniUPnP project
 * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/
 * (c) 2006 Thomas Bernard
 * This software is subject to the conditions detailed
 * in the LICENCE file provided within the distribution */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <syslog.h>
#include "config.h"
#include "upnpdescstrings.h"
#include "miniupnpdpath.h"
#include "upnphttp.h"
#include "upnpglobalvars.h"
#include "minissdp.h"
#include "codelength.h"

extern unsigned char g_notify_interval;
/* SSDP ip/port */
#define SSDP_PORT (1900)
#define SSDP_MCAST_ADDR ("239.255.255.250")

/* different device, different UDN, one to one */
static const char * const known_UDN[] =
{
    ROOTDEVICE_UDN,
    WANCONNDEVICE_UDN,
	WANDEVICE_UDN,
    ROOTDEVICE_UDN,
    ROOTDEVICE_UDN,
    WANCONNDEVICE_UDN,
    WANDEVICE_UDN,
    WANDEVICE_UDN,
    WANCONNDEVICE_UDN,
//    WANCONNDEVICE_UDN,
    ROOTDEVICE_UDN,
    0
};

static const char * const know_uuid[] = 
{
    "uuid:5f6b423f-8864-47ef-b048-9ee05a679f3a",
    "uuid:0026f2fa-95b8-0026-f2fa-95b800000850",
    "uuid:0026f2fa-95b8-0026-f2fa-95b80200e0b8",
    0
};

static int
AddMulticastMembership(int s, in_addr_t ifaddr)
{
	struct ip_mreq imr;	/* Ip multicast membership */

    /* setting up imr structure */
    imr.imr_multiaddr.s_addr = inet_addr(SSDP_MCAST_ADDR);
    /*imr.imr_interface.s_addr = htonl(INADDR_ANY);*/
    imr.imr_interface.s_addr = ifaddr;	/*inet_addr(ifaddr);*/

	if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&imr, sizeof(struct ip_mreq)) < 0)
	{
        syslog(LOG_ERR, "setsockopt(udp, IP_ADD_MEMBERSHIP): %m");
		return -1;
    }

	return 0;
}

/* Open and configure the socket listening for
 * SSDP udp packets sent on 239.255.255.250 port 1900 */
int
OpenAndConfSSDPReceiveSocket()
{
	int s;
	int i = 1;
	struct sockaddr_in sockname;

	if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
	{
		syslog(LOG_ERR, "socket(udp): %m");
		return -1;
	}

	memset(&sockname, 0, sizeof(struct sockaddr_in));
    sockname.sin_family = AF_INET;
    sockname.sin_port = htons(SSDP_PORT);
	/* NOTE : it seems it doesnt work when binding on the specific address */
    /*sockname.sin_addr.s_addr = inet_addr(UPNP_MCAST_ADDR);*/
    sockname.sin_addr.s_addr = htonl(INADDR_ANY);
    /*sockname.sin_addr.s_addr = inet_addr(ifaddr);*/

	/*
 	 * TBS_TAG: by zhaihaichen 2010-3-16
	 * DESC: 设置SO_REUSEADDR解决和dlna共存问题
	 */
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&i, sizeof(i)))
    {
		syslog(LOG_ERR, "setsockopt(udp): %m");
        close(s);
        return -1;
    }
	/*
	 * TBS_END_TAG
	 */

    if(bind(s, (struct sockaddr *)&sockname, sizeof(struct sockaddr_in)) < 0)
	{
		syslog(LOG_ERR, "bind(udp): %m");
		close(s);
		return -1;
    }

	i = n_lan_addr;
	while(i>0)
	{
		i--;
		if(AddMulticastMembership(s, lan_addr[i].addr.s_addr) < 0)
		{
			syslog(LOG_WARNING,
			       "Failed to add multicast membership for address %s",
			       lan_addr[i].str );
		}
	}

	return s;
}

/* open the UDP socket used to send SSDP notifications to
 * the multicast group reserved for them */
static int
OpenAndConfSSDPNotifySocket(in_addr_t addr, unsigned char ittl)/*fuhuoping add ittl*/
{
	int s;
	unsigned char loopchar = 0;
	int bcast = 1;
	struct in_addr mc_if;
	struct sockaddr_in sockname;

	if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
	{
		syslog(LOG_ERR, "socket(udp_notify): %m");
		return -1;
	}

	mc_if.s_addr = addr;	/*inet_addr(addr);*/

	if(setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP, (char *)&loopchar, sizeof(loopchar)) < 0)
	{
		syslog(LOG_ERR, "setsockopt(udp_notify, IP_MULTICAST_LOOP): %m");
		close(s);
		return -1;
	}
    
	if(setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, (char *)&mc_if, sizeof(mc_if)) < 0)
	{
		syslog(LOG_ERR, "setsockopt(udp_notify, IP_MULTICAST_IF): %m");
		close(s);
		return -1;
	}
    /*fuhuoping add TTL*/
	if(setsockopt(s,IPPROTO_IP,IP_MULTICAST_TTL, (char*)&ittl, sizeof(ittl)) < 0)
	{
	    syslog(LOG_ERR, "setsockopt(udp_notify, IP_MULTICAST_TTL): %m");
		close(s);
		return -1;
	}
	if(setsockopt(s, SOL_SOCKET, SO_BROADCAST, &bcast, sizeof(bcast)) < 0)
	{
		syslog(LOG_ERR, "setsockopt(udp_notify, SO_BROADCAST): %m");
		close(s);
		return -1;
	}

	memset(&sockname, 0, sizeof(struct sockaddr_in));
    sockname.sin_family = AF_INET;
    sockname.sin_addr.s_addr = addr;	/*inet_addr(addr);*/

    if (bind(s, (struct sockaddr *)&sockname, sizeof(struct sockaddr_in)) < 0)
	{
		syslog(LOG_ERR, "bind(udp_notify): %m");
		close(s);
		return -1;
    }
	return s;
}

int
OpenAndConfSSDPNotifySockets(int * sockets, unsigned char ittl)
/*OpenAndConfSSDPNotifySockets(int * sockets,
                             struct lan_addr_s * lan_addr, int n_lan_addr)*/
{
	int i, j;
	for(i=0; i<n_lan_addr; i++)
	{
		sockets[i] = OpenAndConfSSDPNotifySocket(lan_addr[i].addr.s_addr,ittl);
		if(sockets[i] < 0)
		{
			for(j=0; j<i; j++)
			{
				close(sockets[j]);
				sockets[j] = -1;
			}
			return -1;
		}
	}
	return 0;
}

/*
 * response from a LiveBox (Wanadoo)
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Thu, 01 Jan 1970 04:03:23 GMT
EXT:
LOCATION: http://192.168.0.1:49152/gatedesc.xml
SERVER: Linux/2.4.17, UPnP/1.0, Intel SDK for UPnP devices /1.2
ST: upnp:rootdevice
USN: uuid:75802409-bccb-40e7-8e6c-fa095ecce13e::upnp:rootdevice

 * response from a Linksys 802.11b :
HTTP/1.1 200 OK
Cache-Control:max-age=120
Location:http://192.168.5.1:5678/rootDesc.xml
Server:NT/5.0 UPnP/1.0
ST:upnp:rootdevice
USN:uuid:upnp-InternetGatewayDevice-1_0-0090a2777777::upnp:rootdevice
EXT:
 */

/* not really an SSDP "announce" as it is the response
 * to a SSDP "M-SEARCH" */
static void
SendSSDPAnnounce2(int s, struct sockaddr_in sockname,
                  const char * st, int st_len, const char * suffix, const char * udn,
                  const char * host, unsigned short port)
{
	int l, n;
	char buf[512];
	/*
	 * follow guideline from document "UPnP Device Architecture 1.0"
	 * uppercase is recommended.
	 * DATE: is recommended
	 * SERVER: OS/ver UPnP/1.0 miniupnpd/1.0
	 * - check what to put in the 'Cache-Control' header
	 * */
	if(0 == memcmp("uuid:", st, 5))
    	l = snprintf(buf, sizeof(buf), "HTTP/1.1 200 OK\r\n"
    		"CACHE-CONTROL: max-age=%d\r\n"
    		/*"DATE: ...\r\n"*/
    		"ST: %.*s%s\r\n"
    		"USN: %.*s%s\r\n"
    		"EXT:\r\n"
    		"SERVER: " MINIUPNPD_SERVER_STRING "\r\n"
    		"LOCATION: http://%s:%u" ROOTDESC_PATH "\r\n"
    		"\r\n",
    		g_notify_interval,
    		st_len, st, suffix,
    		st_len, st, suffix,
    		host, (unsigned int)port);
    else
    	l = snprintf(buf, sizeof(buf), "HTTP/1.1 200 OK\r\n"
    		"CACHE-CONTROL: max-age=%d\r\n"
    		/*"DATE: ...\r\n"*/
    		"ST: %.*s%s\r\n"
    		"USN: %s::%.*s%s\r\n"
    		"EXT:\r\n"
    		"SERVER: " MINIUPNPD_SERVER_STRING "\r\n"
    		"LOCATION: http://%s:%u" ROOTDESC_PATH "\r\n"
    		"\r\n",
    		g_notify_interval,
    		st_len, st, suffix,
    		udn, st_len, st, suffix,
    		host, (unsigned int)port);
	n = sendto(s, buf, l, 0,
	           (struct sockaddr *)&sockname, sizeof(struct sockaddr_in) );
	if(n < 0)
	{
		syslog(LOG_ERR, "sendto(udp): %m");
	}
}

static const char * const known_service_types[] =
{
    ROOTDEVICE_UDN,
	WANCONNDEVICE_UDN,
	WANDEVICE_UDN,
	"upnp:rootdevice",
	"urn:schemas-upnp-org:device:InternetGatewayDevice:",
	"urn:schemas-upnp-org:device:WANConnectionDevice:",
	"urn:schemas-upnp-org:device:WANDevice:",
	"urn:schemas-upnp-org:service:WANCommonInterfaceConfig:",
	"urn:schemas-upnp-org:service:WANIPConnection:",
//	"urn:schemas-upnp-org:service:WANPPPConnection:",
	"urn:schemas-upnp-org:service:Layer3Forwarding:",
	0
};

static void
SendSSDPNotifies(int s, const char * host, unsigned short port,
                 unsigned int lifetime)
{
	struct sockaddr_in sockname;
	int l, n, i=0;
	char bufr[512];

	memset(&sockname, 0, sizeof(struct sockaddr_in));
	sockname.sin_family = AF_INET;
	sockname.sin_port = htons(SSDP_PORT);
	sockname.sin_addr.s_addr = inet_addr(SSDP_MCAST_ADDR);

	while(known_service_types[i])
	{
        if(i < 3)
    		l = snprintf(bufr, sizeof(bufr),
    				"NOTIFY * HTTP/1.1\r\n"
    				"HOST:%s:%d\r\n"
    				"Cache-Control:max-age=%u\r\n"
    				"Location:http://%s:%d" ROOTDESC_PATH"\r\n"
    				/*"Server:miniupnpd/1.0 UPnP/1.0\r\n"*/
    				"Server: " MINIUPNPD_SERVER_STRING "\r\n"
    				"NT:%s%s\r\n"
    				"USN:%s%s\r\n"
    				"NTS:ssdp:alive\r\n"
    				"\r\n",
    				SSDP_MCAST_ADDR, SSDP_PORT,
    				lifetime,
    				host, port,
    				known_service_types[i], "",
                    known_service_types[i], "" );
        else
    		l = snprintf(bufr, sizeof(bufr),
    				"NOTIFY * HTTP/1.1\r\n"
    				"HOST:%s:%d\r\n"
    				"Cache-Control:max-age=%u\r\n"
    				"Location:http://%s:%d" ROOTDESC_PATH"\r\n"
    				/*"Server:miniupnpd/1.0 UPnP/1.0\r\n"*/
    				"Server: " MINIUPNPD_SERVER_STRING "\r\n"
    				"NT:%s%s\r\n"
    				"USN:%s::%s%s\r\n"
    				"NTS:ssdp:alive\r\n"
    				"\r\n",
    				SSDP_MCAST_ADDR, SSDP_PORT,
    				lifetime,
    				host, port,
    				known_service_types[i], (i==3?"":"1"),
    				known_UDN[i], known_service_types[i], (i==3?"":"1") );
		if(l>=sizeof(bufr))
		{
			syslog(LOG_WARNING, "SendSSDPNotifies(): truncated output");
			l = sizeof(bufr);
		}
		n = sendto(s, bufr, l, 0,
			(struct sockaddr *)&sockname, sizeof(struct sockaddr_in) );
		if(n < 0)
		{
			syslog(LOG_ERR, "sendto(udp_notify=%d, %s): %m", s, host);
		}
		i++;
	}
}

void
SendSSDPNotifies2(int * sockets,
                  unsigned short port,
                  unsigned int lifetime)
/*SendSSDPNotifies2(int * sockets, struct lan_addr_s * lan_addr, int n_lan_addr,
                  unsigned short port,
                  unsigned int lifetime)*/
{
	int i;
	for(i=0; i<n_lan_addr; i++)
	{
		SendSSDPNotifies(sockets[i], lan_addr[i].str, port, lifetime);
	}
}

/* ProcessSSDPRequest()
 * process SSDP M-SEARCH requests and responds to them */
void
ProcessSSDPRequest(int s, unsigned short port)
/*ProcessSSDPRequest(int s, struct lan_addr_s * lan_addr, int n_lan_addr,
                   unsigned short port)*/
{
	int n;
	char bufr[1500];
	socklen_t len_r;
	struct sockaddr_in sendername;
	int i, l;
	int lan_addr_index = 0;
	char * st = 0;
	int st_len = 0;
    char * man = 0;
    int man_len = 0;
    char * mx = 0;
    int mx_len = 0;
	len_r = sizeof(struct sockaddr_in);

	n = recvfrom(s, bufr, sizeof(bufr), 0,
	             (struct sockaddr *)&sendername, &len_r);
	if(n < 0)
	{
		syslog(LOG_ERR, "recvfrom(udp): %m");
		return;
	}

	if(memcmp(bufr, "NOTIFY", 6) == 0)
	{
		/* ignore NOTIFY packets. We could log the sender and device type */
		return;
	}
    /* we such check "M-SEARCH * HTTP/1.1" with anything problem?! */
	else if(memcmp(bufr, "M-SEARCH * HTTP/1.1", 19) == 0)
	{
		i = 0;
		while(i < n)
		{
			while(bufr[i] != '\r' || bufr[i+1] != '\n')
				i++;
			i += 2;
			if(strncasecmp(bufr+i, "st:", 3) == 0)
			{
				st = bufr+i+3;
				st_len = 0;
				while(*st == ' ' || *st == '\t') st++;
				while(st[st_len]!='\r' && st[st_len]!='\n') st_len++;
				/*syslog(LOG_INFO, "ST: %.*s", st_len, st);*/
				/*j = 0;*/
				/*while(bufr[i+j]!='\r') j++;*/
				/*syslog(LOG_INFO, "%.*s", j, bufr+i);*/
			}
            else if(strncasecmp(bufr+i, "man:", 4) == 0)
            {
                man = bufr+i+4;
                while(*man == ' ' || *man == '\t') man++;
				while(man[man_len]!='\r' && man[man_len]!='\n') man_len++;
            }
            else if(strncasecmp(bufr+i, "mx:", 3) == 0)
            {
                mx = bufr+i+3;
                while(*mx == ' ' || *mx == '\t') mx++;
                while(mx[mx_len] != '\r' && mx[mx_len] != '\n')
                {
                    /* check "MX" whether is Positive integer */
                    if(mx[mx_len] == '.' || mx[mx_len] == '-')
                        return;
                    mx_len++;
                }
            }
		}
		/*syslog(LOG_INFO, "SSDP M-SEARCH packet received from %s:%d",
	           inet_ntoa(sendername.sin_addr),
	           ntohs(sendername.sin_port) );*/
        /* check "Man" and "MX" whether is absent */
        if(0 == man_len || 0 == mx_len)
            return;
            
        if(st)
		{
			/* TODO : doesnt answer at once but wait for a random time */
			syslog(LOG_INFO, "SSDP M-SEARCH from %s:%d ST: %.*s",
	        	   inet_ntoa(sendername.sin_addr),
	           	   ntohs(sendername.sin_port),
				   st_len, st);
			/* find in which sub network the client is */
			for(i = 0; i<n_lan_addr; i++)
			{
				if( (sendername.sin_addr.s_addr & lan_addr[i].mask.s_addr)
				   == (lan_addr[i].addr.s_addr & lan_addr[i].mask.s_addr))
				{
					lan_addr_index = i;
					break;
				}
			}
			/* Responds to request with a device as ST header */
			for(i = 3; known_service_types[i]; i++)
			{
				l = (int)strlen(known_service_types[i]);
				if((l==st_len || (l < st_len && st[st_len-1] == '1')) && (0 == memcmp(st, known_service_types[i], l)))
				{
					SendSSDPAnnounce2(s, sendername,
					                  st, st_len, "", known_UDN[i],
					                  lan_addr[lan_addr_index].str, port);
					break;
				}
			}
			/* Responds to request with ST: ssdp:all */
			/* strlen("ssdp:all") == 8 */
			if(st_len==8 && (0 == memcmp(st, "ssdp:all", 8)))
			{
				for(i=0; known_service_types[i]; i++)
				{
					l = (int)strlen(known_service_types[i]);
					SendSSDPAnnounce2(s, sendername,
					                  known_service_types[i], l, i<4?"":"1", known_UDN[i],
					                  lan_addr[lan_addr_index].str, port);
				}
			}
			/* responds to request by UUID value */
            for(i = 0; know_uuid[i] && i<3; i++)
            {
    			l = (int)strlen(know_uuid[i]);
    			if(l==st_len && (0 == memcmp(st, know_uuid[i], l)))
    			{
    				SendSSDPAnnounce2(s, sendername, st, st_len, "", "",
    				                  lan_addr[lan_addr_index].str, port);
    			}
            }
		}
		else
		{
			syslog(LOG_INFO, "Invalid SSDP M-SEARCH from %s:%d",
	        	   inet_ntoa(sendername.sin_addr), ntohs(sendername.sin_port));
		}
	}
	else
	{
		syslog(LOG_NOTICE, "Unknown udp packet received from %s:%d",
		       inet_ntoa(sendername.sin_addr), ntohs(sendername.sin_port));
	}
}

/* This will broadcast ssdp:byebye notifications to inform
 * the network that UPnP is going down. */
int
SendSSDPGoodbye(int * sockets, int n_sockets)
{
	struct sockaddr_in sockname;
	int n, l;
	int i, j;
	char bufr[512];

    memset(&sockname, 0, sizeof(struct sockaddr_in));
    sockname.sin_family = AF_INET;
    sockname.sin_port = htons(SSDP_PORT);
    sockname.sin_addr.s_addr = inet_addr(SSDP_MCAST_ADDR);

	for(j=0; j<n_sockets; j++)
	{
	    for(i=0; known_service_types[i]; i++)
	    {
	        l = snprintf(bufr, sizeof(bufr),
	                 "NOTIFY * HTTP/1.1\r\n"
	                 "HOST:%s:%d\r\n"
	                 "NT:%s%s\r\n"
	                 "USN:%s::%s%s\r\n"
	                 "NTS:ssdp:byebye\r\n"
	                 "\r\n",
	                 SSDP_MCAST_ADDR, SSDP_PORT,
					 known_service_types[i], (i<4?"":"1"),
	                 known_UDN[i], known_service_types[i], (i<4?"":"1"));
	        n = sendto(sockets[j], bufr, l, 0,
	                   (struct sockaddr *)&sockname, sizeof(struct sockaddr_in) );
			if(n < 0)
			{
				syslog(LOG_ERR, "sendto(udp_shutdown=%d): %m", sockets[j]);
				return -1;
			}
    	}
	}
	return 0;
}

/* SubmitServicesToMiniSSDPD() :
 * register services offered by MiniUPnPd to a running instance of
 * MiniSSDPd */
int
SubmitServicesToMiniSSDPD(const char * host, unsigned short port) {
	struct sockaddr_un addr;
	int s;
	unsigned char buffer[2048];
	char strbuf[256];
	unsigned char * p;
	int i, l;

	s = socket(AF_UNIX, SOCK_STREAM, 0);
	if(s < 0) {
		syslog(LOG_ERR, "socket(unix): %m");
		return -1;
	}
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, minissdpdsocketpath, sizeof(addr.sun_path));
	if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
		syslog(LOG_ERR, "connect(\"%s\"): %m", minissdpdsocketpath);
		return -1;
	}
	for(i = 0; known_service_types[i]; i++) {
		buffer[0] = 4;
		p = buffer + 1;
		l = (int)strlen(known_service_types[i]);
		if(i > 0)
			l++;
		CODELENGTH(l, p);
		memcpy(p, known_service_types[i], l);
		if(i > 0)
			p[l-1] = '1';
		p += l;
		l = snprintf(strbuf, sizeof(strbuf), "%s::%s%s",
		             known_UDN[i], known_service_types[i], (i<4)?"":"1");
		CODELENGTH(l, p);
		memcpy(p, strbuf, l);
		p += l;
		l = (int)strlen(MINIUPNPD_SERVER_STRING);
		CODELENGTH(l, p);
		memcpy(p, MINIUPNPD_SERVER_STRING, l);
		p += l;
		l = snprintf(strbuf, sizeof(strbuf), "http://%s:%u" ROOTDESC_PATH,
		             host, (unsigned int)port);
		CODELENGTH(l, p);
		memcpy(p, strbuf, l);
		p += l;
		if(write(s, buffer, p - buffer) < 0) {
			syslog(LOG_ERR, "write(): %m");
			return -1;
		}
	}
 	close(s);
	return 0;
}
