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 "config.h"
49
50#include <stdio.h>
51#include <string.h>
52#include <errno.h>
53#include <sys/queue.h>
54#include <stdlib.h>
55#include <unistd.h>
56#include <time.h>
57#include <sys/types.h>
58#include <sys/socket.h>
59#include <sys/param.h>
60#include <netinet/in.h>
61#include <arpa/inet.h>
62#include <fcntl.h>
63#include <errno.h>
64
65#include "upnpevents.h"
66#include "minidlnapath.h"
67#include "upnpglobalvars.h"
68#include "upnpdescgen.h"
69#include "uuid.h"
70#include "utils.h"
71#include "log.h"
72
73/* stuctures definitions */
74struct subscriber {
75	LIST_ENTRY(subscriber) entries;
76	struct upnp_event_notify * notify;
77	time_t timeout;
78	uint32_t seq;
79	enum subscriber_service_enum service;
80	char uuid[42];
81	char callback[];
82};
83
84struct upnp_event_notify {
85	LIST_ENTRY(upnp_event_notify) entries;
86    int s;  /* socket */
87    enum { ECreated=1,
88	       EConnecting,
89	       ESending,
90	       EWaitingForResponse,
91	       EFinished,
92	       EError } state;
93    struct subscriber * sub;
94    char * buffer;
95    int buffersize;
96	int tosend;
97    int sent;
98	const char * path;
99	char addrstr[16];
100	char portstr[8];
101};
102
103/* prototypes */
104static void
105upnp_event_create_notify(struct subscriber * sub);
106
107/* Subscriber list */
108LIST_HEAD(listhead, subscriber) subscriberlist = { NULL };
109
110/* notify list */
111LIST_HEAD(listheadnotif, upnp_event_notify) notifylist = { NULL };
112
113/* create a new subscriber */
114static struct subscriber *
115newSubscriber(const char * eventurl, const char * callback, int callbacklen)
116{
117	struct subscriber * tmp;
118	if(!eventurl || !callback || !callbacklen)
119		return NULL;
120	tmp = calloc(1, sizeof(struct subscriber)+callbacklen+1);
121	if(strcmp(eventurl, CONTENTDIRECTORY_EVENTURL)==0)
122		tmp->service = EContentDirectory;
123	else if(strcmp(eventurl, CONNECTIONMGR_EVENTURL)==0)
124		tmp->service = EConnectionManager;
125	else if(strcmp(eventurl, X_MS_MEDIARECEIVERREGISTRAR_EVENTURL)==0)
126		tmp->service = EMSMediaReceiverRegistrar;
127	else {
128		free(tmp);
129		return NULL;
130	}
131	memcpy(tmp->callback, callback, callbacklen);
132	tmp->callback[callbacklen] = '\0';
133	/* make a dummy uuid */
134	strncpyt(tmp->uuid, uuidvalue, sizeof(tmp->uuid));
135	if( get_uuid_string(tmp->uuid+5) != 0 )
136	{
137		tmp->uuid[sizeof(tmp->uuid)-1] = '\0';
138		snprintf(tmp->uuid+37, 5, "%04lx", random() & 0xffff);
139	}
140
141	return tmp;
142}
143
144/* creates a new subscriber and adds it to the subscriber list
145 * also initiate 1st notify */
146const char *
147upnpevents_addSubscriber(const char * eventurl,
148                         const char * callback, int callbacklen,
149                         int timeout)
150{
151	struct subscriber * tmp;
152	DPRINTF(E_DEBUG, L_HTTP, "addSubscriber(%s, %.*s, %d)\n",
153	       eventurl, callbacklen, callback, timeout);
154	tmp = newSubscriber(eventurl, callback, callbacklen);
155	if(!tmp)
156		return NULL;
157	if(timeout)
158		tmp->timeout = uptime() + timeout;
159	LIST_INSERT_HEAD(&subscriberlist, tmp, entries);
160	upnp_event_create_notify(tmp);
161	return tmp->uuid;
162}
163
164/* renew a subscription (update the timeout) */
165int
166renewSubscription(const char * sid, int sidlen, int timeout)
167{
168	struct subscriber * sub;
169	for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
170		if(memcmp(sid, sub->uuid, 41) == 0) {
171			sub->timeout = (timeout ? uptime() + timeout : 0);
172			return 0;
173		}
174	}
175	return -1;
176}
177
178int
179upnpevents_removeSubscriber(const char * sid, int sidlen)
180{
181	struct subscriber * sub;
182	if(!sid)
183		return -1;
184	DPRINTF(E_DEBUG, L_HTTP, "removeSubscriber(%.*s)\n",
185	       sidlen, sid);
186	for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
187		if(memcmp(sid, sub->uuid, 41) == 0) {
188			if(sub->notify) {
189				sub->notify->sub = NULL;
190			}
191			LIST_REMOVE(sub, entries);
192			free(sub);
193			return 0;
194		}
195	}
196	return -1;
197}
198
199void
200upnpevents_removeSubscribers(void)
201{
202	struct subscriber * sub;
203
204	for(sub = subscriberlist.lh_first; sub != NULL; sub = subscriberlist.lh_first) {
205		upnpevents_removeSubscriber(sub->uuid, sizeof(sub->uuid));
206	}
207}
208
209/* notifies all subscribers of a SystemUpdateID change */
210void
211upnp_event_var_change_notify(enum subscriber_service_enum service)
212{
213	struct subscriber * sub;
214	for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
215		if(sub->service == service && sub->notify == NULL)
216			upnp_event_create_notify(sub);
217	}
218}
219
220/* create and add the notify object to the list */
221static void
222upnp_event_create_notify(struct subscriber * sub)
223{
224	struct upnp_event_notify * obj;
225	int flags;
226	obj = calloc(1, sizeof(struct upnp_event_notify));
227	if(!obj) {
228		DPRINTF(E_ERROR, L_HTTP, "%s: calloc(): %s\n", "upnp_event_create_notify", strerror(errno));
229		return;
230	}
231	obj->sub = sub;
232	obj->state = ECreated;
233	obj->s = socket(PF_INET, SOCK_STREAM, 0);
234	if(obj->s<0) {
235		DPRINTF(E_ERROR, L_HTTP, "%s: socket(): %s\n", "upnp_event_create_notify", strerror(errno));
236		goto error;
237	}
238	if((flags = fcntl(obj->s, F_GETFL, 0)) < 0) {
239		DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_GETFL..): %s\n",
240		       "upnp_event_create_notify", strerror(errno));
241		goto error;
242	}
243	if(fcntl(obj->s, F_SETFL, flags | O_NONBLOCK) < 0) {
244		DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_SETFL..): %s\n",
245		       "upnp_event_create_notify", strerror(errno));
246		goto error;
247	}
248	if(sub)
249		sub->notify = obj;
250	LIST_INSERT_HEAD(&notifylist, obj, entries);
251	return;
252error:
253	if(obj->s >= 0)
254		close(obj->s);
255	free(obj);
256}
257
258static void
259upnp_event_notify_connect(struct upnp_event_notify * obj)
260{
261	int i;
262	const char * p;
263	unsigned short port;
264	struct sockaddr_in addr;
265	if(!obj)
266		return;
267	memset(&addr, 0, sizeof(addr));
268	i = 0;
269	if(obj->sub == NULL) {
270		obj->state = EError;
271		return;
272	}
273	p = obj->sub->callback;
274	p += 7;	/* http:// */
275	while(*p != '/' && *p != ':' && i < (sizeof(obj->addrstr)-1))
276		obj->addrstr[i++] = *(p++);
277	obj->addrstr[i] = '\0';
278	if(*p == ':') {
279		obj->portstr[0] = *p;
280		i = 1;
281		p++;
282		port = (unsigned short)atoi(p);
283		while(*p != '/' && *p != '\0') {
284			if(i<7) obj->portstr[i++] = *p;
285			p++;
286		}
287		obj->portstr[i] = 0;
288	} else {
289		port = 80;
290		obj->portstr[0] = '\0';
291	}
292	if( *p )
293		obj->path = p;
294	else
295		obj->path = "/";
296	addr.sin_family = AF_INET;
297	inet_aton(obj->addrstr, &addr.sin_addr);
298	addr.sin_port = htons(port);
299	DPRINTF(E_DEBUG, L_HTTP, "%s: '%s' %hu '%s'\n", "upnp_event_notify_connect",
300	       obj->addrstr, port, obj->path);
301	obj->state = EConnecting;
302	if(connect(obj->s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
303		if(errno != EINPROGRESS && errno != EWOULDBLOCK) {
304			DPRINTF(E_ERROR, L_HTTP, "%s: connect(): %s\n", "upnp_event_notify_connect", strerror(errno));
305			obj->state = EError;
306		}
307	}
308}
309
310static void upnp_event_prepare(struct upnp_event_notify * obj)
311{
312	static const char notifymsg[] =
313		"NOTIFY %s HTTP/1.1\r\n"
314		"Host: %s%s\r\n"
315		"Content-Type: text/xml; charset=\"utf-8\"\r\n"
316		"Content-Length: %d\r\n"
317		"NT: upnp:event\r\n"
318		"NTS: upnp:propchange\r\n"
319		"SID: %s\r\n"
320		"SEQ: %u\r\n"
321		"Connection: close\r\n"
322		"Cache-Control: no-cache\r\n"
323		"\r\n"
324		"%.*s\r\n";
325	char * xml;
326	int l;
327	if(obj->sub == NULL) {
328		obj->state = EError;
329		return;
330	}
331	switch(obj->sub->service) {
332	case EContentDirectory:
333		xml = getVarsContentDirectory(&l);
334		break;
335	case EConnectionManager:
336		xml = getVarsConnectionManager(&l);
337		break;
338	case EMSMediaReceiverRegistrar:
339		xml = getVarsX_MS_MediaReceiverRegistrar(&l);
340		break;
341	default:
342		xml = NULL;
343		l = 0;
344	}
345	obj->tosend = asprintf(&(obj->buffer), notifymsg,
346	                       obj->path, obj->addrstr, obj->portstr, l+2,
347	                       obj->sub->uuid, obj->sub->seq,
348	                       l, xml);
349	obj->buffersize = obj->tosend;
350	free(xml);
351	DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event response:\n%s\n", obj->buffer);
352	obj->state = ESending;
353}
354
355static void upnp_event_send(struct upnp_event_notify * obj)
356{
357	int i;
358	//DEBUG DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event:\n%s", obj->buffer+obj->sent);
359	while( obj->sent < obj->tosend ) {
360		i = send(obj->s, obj->buffer + obj->sent, obj->tosend - obj->sent, 0);
361		if(i<0) {
362			DPRINTF(E_WARN, L_HTTP, "%s: send(): %s\n", "upnp_event_send", strerror(errno));
363			obj->state = EError;
364			return;
365		}
366		obj->sent += i;
367	}
368	if(obj->sent == obj->tosend)
369		obj->state = EWaitingForResponse;
370}
371
372static void upnp_event_recv(struct upnp_event_notify * obj)
373{
374	int n;
375	n = recv(obj->s, obj->buffer, obj->buffersize, 0);
376	if(n<0) {
377		DPRINTF(E_ERROR, L_HTTP, "%s: recv(): %s\n", "upnp_event_recv", strerror(errno));
378		obj->state = EError;
379		return;
380	}
381	DPRINTF(E_DEBUG, L_HTTP, "%s: (%dbytes) %.*s\n", "upnp_event_recv",
382	       n, n, obj->buffer);
383	obj->state = EFinished;
384	if(obj->sub)
385	{
386		obj->sub->seq++;
387		if (!obj->sub->seq)
388			obj->sub->seq++;
389	}
390}
391
392static void
393upnp_event_process_notify(struct upnp_event_notify * obj)
394{
395	switch(obj->state) {
396	case EConnecting:
397		/* now connected or failed to connect */
398		upnp_event_prepare(obj);
399		upnp_event_send(obj);
400		break;
401	case ESending:
402		upnp_event_send(obj);
403		break;
404	case EWaitingForResponse:
405		upnp_event_recv(obj);
406		break;
407	case EFinished:
408		close(obj->s);
409		obj->s = -1;
410		break;
411	default:
412		DPRINTF(E_ERROR, L_HTTP, "upnp_event_process_notify: unknown state\n");
413	}
414}
415
416void upnpevents_selectfds(fd_set *readset, fd_set *writeset, int * max_fd)
417{
418	struct upnp_event_notify * obj;
419	for(obj = notifylist.lh_first; obj != NULL; obj = obj->entries.le_next) {
420		DPRINTF(E_DEBUG, L_HTTP, "upnpevents_selectfds: %p %d %d\n",
421		       obj, obj->state, obj->s);
422		if(obj->s >= 0) {
423			switch(obj->state) {
424			case ECreated:
425				upnp_event_notify_connect(obj);
426				if(obj->state != EConnecting)
427					break;
428			case EConnecting:
429			case ESending:
430				FD_SET(obj->s, writeset);
431				if(obj->s > *max_fd)
432					*max_fd = obj->s;
433				break;
434			case EWaitingForResponse:
435				FD_SET(obj->s, readset);
436				if(obj->s > *max_fd)
437					*max_fd = obj->s;
438				break;
439			default:
440				break;
441			}
442		}
443	}
444}
445
446void upnpevents_processfds(fd_set *readset, fd_set *writeset)
447{
448	struct upnp_event_notify * obj;
449	struct upnp_event_notify * next;
450	struct subscriber * sub;
451	struct subscriber * subnext;
452	time_t curtime;
453	for(obj = notifylist.lh_first; obj != NULL; obj = obj->entries.le_next) {
454		DPRINTF(E_DEBUG, L_HTTP, "%s: %p %d %d %d %d\n",
455		       "upnpevents_processfds", obj, obj->state, obj->s,
456		       FD_ISSET(obj->s, readset), FD_ISSET(obj->s, writeset));
457		if(obj->s >= 0) {
458			if(FD_ISSET(obj->s, readset) || FD_ISSET(obj->s, writeset))
459				upnp_event_process_notify(obj);
460		}
461	}
462	obj = notifylist.lh_first;
463	while(obj != NULL) {
464		next = obj->entries.le_next;
465		if(obj->state == EError || obj->state == EFinished) {
466			if(obj->s >= 0) {
467				close(obj->s);
468			}
469			if(obj->sub)
470				obj->sub->notify = NULL;
471#if 0 /* Just let it time out instead of explicitly removing the subscriber */
472			/* remove also the subscriber from the list if there was an error */
473			if(obj->state == EError && obj->sub) {
474				LIST_REMOVE(obj->sub, entries);
475				free(obj->sub);
476			}
477#endif
478			free(obj->buffer);
479			LIST_REMOVE(obj, entries);
480			free(obj);
481		}
482		obj = next;
483	}
484	/* remove timeouted subscribers */
485	curtime = uptime();
486	for(sub = subscriberlist.lh_first; sub != NULL; ) {
487		subnext = sub->entries.le_next;
488		if(sub->timeout && curtime > sub->timeout && sub->notify == NULL) {
489			LIST_REMOVE(sub, entries);
490			free(sub);
491		}
492		sub = subnext;
493	}
494}
495
496