1/* MiniDLNA project
2 * http://minidlna.sourceforge.net/
3 *
4 * MiniDLNA media server
5 * Copyright (C) 2008-2009  Justin Maggard
6 *
7 * This file is part of MiniDLNA.
8 *
9 * MiniDLNA is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License version 2 as
11 * published by the Free Software Foundation.
12 *
13 * MiniDLNA is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
20 *
21 * Portions of the code from the MiniUPnP project:
22 *
23 * Copyright (c) 2006-2007, Thomas Bernard
24 * All rights reserved.
25 *
26 * Redistribution and use in source and binary forms, with or without
27 * modification, are permitted provided that the following conditions are met:
28 *     * Redistributions of source code must retain the above copyright
29 *       notice, this list of conditions and the following disclaimer.
30 *     * Redistributions in binary form must reproduce the above copyright
31 *       notice, this list of conditions and the following disclaimer in the
32 *       documentation and/or other materials provided with the distribution.
33 *     * The name of the author may not be used to endorse or promote products
34 *       derived from this software without specific prior written permission.
35 *
36 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
37 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
38 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
40 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
41 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
42 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
43 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
44 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
45 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
46 * POSSIBILITY OF SUCH DAMAGE.
47 */
48#include <stdio.h>
49#include <string.h>
50#include <errno.h>
51#include <sys/queue.h>
52#include <stdlib.h>
53#include <unistd.h>
54#include <time.h>
55#include <sys/types.h>
56#include <sys/socket.h>
57#include <sys/param.h>
58#include <netinet/in.h>
59#include <arpa/inet.h>
60#include <fcntl.h>
61#include <errno.h>
62
63#include "config.h"
64#include "upnpevents.h"
65#include "minidlnapath.h"
66#include "upnpglobalvars.h"
67#include "upnpdescgen.h"
68#include "uuid.h"
69#include "log.h"
70
71/* stuctures definitions */
72struct subscriber {
73	LIST_ENTRY(subscriber) entries;
74	struct upnp_event_notify * notify;
75	time_t timeout;
76	uint32_t seq;
77	enum subscriber_service_enum service;
78	char uuid[42];
79	char callback[];
80};
81
82struct upnp_event_notify {
83	LIST_ENTRY(upnp_event_notify) entries;
84    int s;  /* socket */
85    enum { ECreated=1,
86	       EConnecting,
87	       ESending,
88	       EWaitingForResponse,
89	       EFinished,
90	       EError } state;
91    struct subscriber * sub;
92    char * buffer;
93    int buffersize;
94	int tosend;
95    int sent;
96	const char * path;
97	char addrstr[16];
98	char portstr[8];
99};
100
101/* prototypes */
102static void
103upnp_event_create_notify(struct subscriber * sub);
104
105/* Subscriber list */
106LIST_HEAD(listhead, subscriber) subscriberlist = { NULL };
107
108/* notify list */
109LIST_HEAD(listheadnotif, upnp_event_notify) notifylist = { NULL };
110
111/* create a new subscriber */
112static struct subscriber *
113newSubscriber(const char * eventurl, const char * callback, int callbacklen)
114{
115	struct subscriber * tmp;
116	if(!eventurl || !callback || !callbacklen)
117		return NULL;
118	tmp = calloc(1, sizeof(struct subscriber)+callbacklen+1);
119	if(strcmp(eventurl, CONTENTDIRECTORY_EVENTURL)==0)
120		tmp->service = EContentDirectory;
121	else if(strcmp(eventurl, CONNECTIONMGR_EVENTURL)==0)
122		tmp->service = EConnectionManager;
123	else if(strcmp(eventurl, X_MS_MEDIARECEIVERREGISTRAR_EVENTURL)==0)
124		tmp->service = EMSMediaReceiverRegistrar;
125	else {
126		free(tmp);
127		return NULL;
128	}
129	memcpy(tmp->callback, callback, callbacklen);
130	tmp->callback[callbacklen] = '\0';
131	/* make a dummy uuid */
132	strncpy(tmp->uuid, uuidvalue, sizeof(tmp->uuid));
133	if( get_uuid_string(tmp->uuid+5) != 0 )
134	{
135		tmp->uuid[sizeof(tmp->uuid)-1] = '\0';
136		snprintf(tmp->uuid+37, 5, "%04lx", random() & 0xffff);
137	}
138
139	return tmp;
140}
141
142/* creates a new subscriber and adds it to the subscriber list
143 * also initiate 1st notify */
144const char *
145upnpevents_addSubscriber(const char * eventurl,
146                         const char * callback, int callbacklen,
147                         int timeout)
148{
149	struct subscriber * tmp;
150	DPRINTF(E_DEBUG, L_HTTP, "addSubscriber(%s, %.*s, %d)\n",
151	       eventurl, callbacklen, callback, timeout);
152	tmp = newSubscriber(eventurl, callback, callbacklen);
153	if(!tmp)
154		return NULL;
155	if(timeout)
156		tmp->timeout = time(NULL) + timeout;
157	LIST_INSERT_HEAD(&subscriberlist, tmp, entries);
158	upnp_event_create_notify(tmp);
159	return tmp->uuid;
160}
161
162/* renew a subscription (update the timeout) */
163int
164renewSubscription(const char * sid, int sidlen, int timeout)
165{
166	struct subscriber * sub;
167	for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
168		if(memcmp(sid, sub->uuid, 41) == 0) {
169			sub->timeout = (timeout ? time(NULL) + timeout : 0);
170			return 0;
171		}
172	}
173	return -1;
174}
175
176int
177upnpevents_removeSubscriber(const char * sid, int sidlen)
178{
179	struct subscriber * sub;
180	if(!sid)
181		return -1;
182	DPRINTF(E_DEBUG, L_HTTP, "removeSubscriber(%.*s)\n",
183	       sidlen, sid);
184	for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
185		if(memcmp(sid, sub->uuid, 41) == 0) {
186			if(sub->notify) {
187				sub->notify->sub = NULL;
188			}
189			LIST_REMOVE(sub, entries);
190			free(sub);
191			return 0;
192		}
193	}
194	return -1;
195}
196
197void
198upnpevents_removeSubscribers(void)
199{
200	struct subscriber * sub;
201
202	for(sub = subscriberlist.lh_first; sub != NULL; sub = subscriberlist.lh_first) {
203		upnpevents_removeSubscriber(sub->uuid, sizeof(sub->uuid));
204	}
205}
206
207/* notifies all subscribers of a SystemUpdateID change */
208void
209upnp_event_var_change_notify(enum subscriber_service_enum service)
210{
211	struct subscriber * sub;
212	for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
213		if(sub->service == service && sub->notify == NULL)
214			upnp_event_create_notify(sub);
215	}
216}
217
218/* create and add the notify object to the list */
219static void
220upnp_event_create_notify(struct subscriber * sub)
221{
222	struct upnp_event_notify * obj;
223	int flags;
224	obj = calloc(1, sizeof(struct upnp_event_notify));
225	if(!obj) {
226		DPRINTF(E_ERROR, L_HTTP, "%s: calloc(): %s\n", "upnp_event_create_notify", strerror(errno));
227		return;
228	}
229	obj->sub = sub;
230	obj->state = ECreated;
231	obj->s = socket(PF_INET, SOCK_STREAM, 0);
232	if(obj->s<0) {
233		DPRINTF(E_ERROR, L_HTTP, "%s: socket(): %s\n", "upnp_event_create_notify", strerror(errno));
234		goto error;
235	}
236	if((flags = fcntl(obj->s, F_GETFL, 0)) < 0) {
237		DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_GETFL..): %s\n",
238		       "upnp_event_create_notify", strerror(errno));
239		goto error;
240	}
241	if(fcntl(obj->s, F_SETFL, flags | O_NONBLOCK) < 0) {
242		DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_SETFL..): %s\n",
243		       "upnp_event_create_notify", strerror(errno));
244		goto error;
245	}
246	if(sub)
247		sub->notify = obj;
248	LIST_INSERT_HEAD(&notifylist, obj, entries);
249	return;
250error:
251	if(obj->s >= 0)
252		close(obj->s);
253	free(obj);
254}
255
256static void
257upnp_event_notify_connect(struct upnp_event_notify * obj)
258{
259	int i;
260	const char * p;
261	unsigned short port;
262	struct sockaddr_in addr;
263	if(!obj)
264		return;
265	memset(&addr, 0, sizeof(addr));
266	i = 0;
267	if(obj->sub == NULL) {
268		obj->state = EError;
269		return;
270	}
271	p = obj->sub->callback;
272	p += 7;	/* http:// */
273	while(*p != '/' && *p != ':' && i < (sizeof(obj->addrstr)-1))
274		obj->addrstr[i++] = *(p++);
275	obj->addrstr[i] = '\0';
276	if(*p == ':') {
277		obj->portstr[0] = *p;
278		i = 1;
279		p++;
280		port = (unsigned short)atoi(p);
281		while(*p != '/' && *p != '\0') {
282			if(i<7) obj->portstr[i++] = *p;
283			p++;
284		}
285		obj->portstr[i] = 0;
286	} else {
287		port = 80;
288		obj->portstr[0] = '\0';
289	}
290	if( *p )
291		obj->path = p;
292	else
293		obj->path = "/";
294	addr.sin_family = AF_INET;
295	inet_aton(obj->addrstr, &addr.sin_addr);
296	addr.sin_port = htons(port);
297	DPRINTF(E_DEBUG, L_HTTP, "%s: '%s' %hu '%s'\n", "upnp_event_notify_connect",
298	       obj->addrstr, port, obj->path);
299	obj->state = EConnecting;
300	if(connect(obj->s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
301		if(errno != EINPROGRESS && errno != EWOULDBLOCK) {
302			DPRINTF(E_ERROR, L_HTTP, "%s: connect(): %s\n", "upnp_event_notify_connect", strerror(errno));
303			obj->state = EError;
304		}
305	}
306}
307
308static void upnp_event_prepare(struct upnp_event_notify * obj)
309{
310	static const char notifymsg[] =
311		"NOTIFY %s HTTP/1.1\r\n"
312		"Host: %s%s\r\n"
313		"Content-Type: text/xml; charset=\"utf-8\"\r\n"
314		"Content-Length: %d\r\n"
315		"NT: upnp:event\r\n"
316		"NTS: upnp:propchange\r\n"
317		"SID: %s\r\n"
318		"SEQ: %u\r\n"
319		"Connection: close\r\n"
320		"Cache-Control: no-cache\r\n"
321		"\r\n"
322		"%.*s\r\n";
323	char * xml;
324	int l;
325	if(obj->sub == NULL) {
326		obj->state = EError;
327		return;
328	}
329	switch(obj->sub->service) {
330	case EContentDirectory:
331		xml = getVarsContentDirectory(&l);
332		break;
333	case EConnectionManager:
334		xml = getVarsConnectionManager(&l);
335		break;
336	case EMSMediaReceiverRegistrar:
337		xml = getVarsX_MS_MediaReceiverRegistrar(&l);
338		break;
339	default:
340		xml = NULL;
341		l = 0;
342	}
343	obj->tosend = asprintf(&(obj->buffer), notifymsg,
344	                       obj->path, obj->addrstr, obj->portstr, l+2,
345	                       obj->sub->uuid, obj->sub->seq,
346	                       l, xml);
347	obj->buffersize = obj->tosend;
348	if(xml) {
349		free(xml);
350		xml = NULL;
351	}
352	DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event response:\n%s\n", obj->buffer);
353	obj->state = ESending;
354}
355
356static void upnp_event_send(struct upnp_event_notify * obj)
357{
358	int i;
359	//DEBUG DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event:\n%s", obj->buffer+obj->sent);
360	while( obj->sent < obj->tosend ) {
361		i = send(obj->s, obj->buffer + obj->sent, obj->tosend - obj->sent, 0);
362		if(i<0) {
363			DPRINTF(E_WARN, L_HTTP, "%s: send(): %s\n", "upnp_event_send", strerror(errno));
364			obj->state = EError;
365			return;
366		}
367		obj->sent += i;
368	}
369	if(obj->sent == obj->tosend)
370		obj->state = EWaitingForResponse;
371}
372
373static void upnp_event_recv(struct upnp_event_notify * obj)
374{
375	int n;
376	n = recv(obj->s, obj->buffer, obj->buffersize, 0);
377	if(n<0) {
378		DPRINTF(E_ERROR, L_HTTP, "%s: recv(): %s\n", "upnp_event_recv", strerror(errno));
379		obj->state = EError;
380		return;
381	}
382	DPRINTF(E_DEBUG, L_HTTP, "%s: (%dbytes) %.*s\n", "upnp_event_recv",
383	       n, n, obj->buffer);
384	obj->state = EFinished;
385	if(obj->sub)
386	{
387		obj->sub->seq++;
388		if (!obj->sub->seq)
389			obj->sub->seq++;
390	}
391}
392
393static void
394upnp_event_process_notify(struct upnp_event_notify * obj)
395{
396	switch(obj->state) {
397	case EConnecting:
398		/* now connected or failed to connect */
399		upnp_event_prepare(obj);
400		upnp_event_send(obj);
401		break;
402	case ESending:
403		upnp_event_send(obj);
404		break;
405	case EWaitingForResponse:
406		upnp_event_recv(obj);
407		break;
408	case EFinished:
409		close(obj->s);
410		obj->s = -1;
411		break;
412	default:
413		DPRINTF(E_ERROR, L_HTTP, "upnp_event_process_notify: unknown state\n");
414	}
415}
416
417void upnpevents_selectfds(fd_set *readset, fd_set *writeset, int * max_fd)
418{
419	struct upnp_event_notify * obj;
420	for(obj = notifylist.lh_first; obj != NULL; obj = obj->entries.le_next) {
421		DPRINTF(E_DEBUG, L_HTTP, "upnpevents_selectfds: %p %d %d\n",
422		       obj, obj->state, obj->s);
423		if(obj->s >= 0) {
424			switch(obj->state) {
425			case ECreated:
426				upnp_event_notify_connect(obj);
427				if(obj->state != EConnecting)
428					break;
429			case EConnecting:
430			case ESending:
431				FD_SET(obj->s, writeset);
432				if(obj->s > *max_fd)
433					*max_fd = obj->s;
434				break;
435			case EWaitingForResponse:
436				FD_SET(obj->s, readset);
437				if(obj->s > *max_fd)
438					*max_fd = obj->s;
439				break;
440			default:
441				break;
442			}
443		}
444	}
445}
446
447void upnpevents_processfds(fd_set *readset, fd_set *writeset)
448{
449	struct upnp_event_notify * obj;
450	struct upnp_event_notify * next;
451	struct subscriber * sub;
452	struct subscriber * subnext;
453	time_t curtime;
454	for(obj = notifylist.lh_first; obj != NULL; obj = obj->entries.le_next) {
455		DPRINTF(E_DEBUG, L_HTTP, "%s: %p %d %d %d %d\n",
456		       "upnpevents_processfds", obj, obj->state, obj->s,
457		       FD_ISSET(obj->s, readset), FD_ISSET(obj->s, writeset));
458		if(obj->s >= 0) {
459			if(FD_ISSET(obj->s, readset) || FD_ISSET(obj->s, writeset))
460				upnp_event_process_notify(obj);
461		}
462	}
463	obj = notifylist.lh_first;
464	while(obj != NULL) {
465		next = obj->entries.le_next;
466		if(obj->state == EError || obj->state == EFinished) {
467			if(obj->s >= 0) {
468				close(obj->s);
469			}
470			if(obj->sub)
471				obj->sub->notify = NULL;
472#if 0 /* Just let it time out instead of explicitly removing the subscriber */
473			/* remove also the subscriber from the list if there was an error */
474			if(obj->state == EError && obj->sub) {
475				LIST_REMOVE(obj->sub, entries);
476				free(obj->sub);
477			}
478#endif
479			if(obj->buffer) {
480				free(obj->buffer);
481			}
482			LIST_REMOVE(obj, entries);
483			free(obj);
484		}
485		obj = next;
486	}
487	/* remove timeouted subscribers */
488	curtime = time(NULL);
489	for(sub = subscriberlist.lh_first; sub != NULL; ) {
490		subnext = sub->entries.le_next;
491		if(sub->timeout && curtime > sub->timeout && sub->notify == NULL) {
492			LIST_REMOVE(sub, entries);
493			free(sub);
494		}
495		sub = subnext;
496	}
497}
498
499