minidlna/upnpevents.c
Gleb Smirnoff f9a78d598e Support for pluggable event modules, stage 1. Take out select() loop and associated code into a separate module select.c. Detailed list of changes down below.
The idea is taken from the nginx web server, but much simplified and
almost no copypaste left. This will allow minidlna to use different
event dispatcher APIs, which would be defined at compile time.

My personal goal is to convert minidlna to kqueue(2) on FreeBSD. This
would later allow for kqueue based directory change notification, which
won't conflict with select(2) like the current patch does.
Other platforms will also benefit from the pluggability of the event
system, Linux can switch to epoll(2) or at least to poll(2).

Detailed list of changes:

* event.h [New]
  Our internal API to unify different event dispatch systems.
* select.c [New]
  Much simplified version of nginx's ngx_select_module.c.
* minidlna.c
  - Split out listen socket event processing into separate function
    ProcessListen(), which matches event_process_t type.
  - Create and initialize struct event for the monitor socket, SSDP
    socket, HTTP socket and beacon socket.
  - Simplify and make more precise timeout calculation using
    helper timeval functions from utils.c. Treat gettimeofday() error
    as a fatal event.
  - Rip out all stuff related to select(2). Just call event_module.process().
* upnpevents.c
  - Embed struct event into upnp_event_notify.
  - Merge upnp_event_create_notify() with upnp_event_notify_connect().
    Start connecting immediately after socket creation. Garbage collect
    now useless ECreated state.
  - Make upnp_event_process_notify() of event_process_t type, and use it
    as process callback for upnp_event_notify event.
  - Looks like we always create upnp_event_notify with existing subscriber,
    and never clear it later. Remove checks for obj->sub and assert that it
    is never NULL. Simplifies things.
  - When switching obj state, add/del it to event dispatcher accrodingly.
  - Garbage collect upnpevents_selectfds().
  - Garbage collect select(2) related stuff from upnpevents_processfds().
    Rename function to upnpevents_gc(), since the remaining functionality
    is garbage collecting, not file descriptor processing.
    Actually, this can be simplified even more. We can safely close sockets
    and free objects immediately, eliminating need for upnpevents_gc(). But
    this change would be beyond scope of this commit.
* upnphttp.c, upnphttp.h
  Embed struct event into struct upnphttp. Adjust Process_upnphttp() to match
  event_process_t type. Add/del to event dispatcher once creating/closing a
  socket.
* minissdp.c, minissdp.h
  Make ProcessSSDPRequest() of event_process_t type.
* getifaddr.c, getifaddr.h
  Make ProcessMonitorEvent() of event_process_t type.
2018-01-16 16:53:08 -08:00

470 lines
12 KiB
C

/* MiniDLNA project
* http://minidlna.sourceforge.net/
*
* MiniDLNA media server
* Copyright (C) 2008-2009 Justin Maggard
*
* This file is part of MiniDLNA.
*
* MiniDLNA is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* MiniDLNA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
*
* Portions of the code from the MiniUPnP project:
*
* Copyright (c) 2006-2007, Thomas Bernard
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/queue.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <fcntl.h>
#include <errno.h>
#include "event.h"
#include "upnpevents.h"
#include "minidlnapath.h"
#include "upnpglobalvars.h"
#include "upnpdescgen.h"
#include "uuid.h"
#include "utils.h"
#include "log.h"
/* stuctures definitions */
struct subscriber {
LIST_ENTRY(subscriber) entries;
struct upnp_event_notify * notify;
time_t timeout;
uint32_t seq;
enum subscriber_service_enum service;
char uuid[42];
char callback[];
};
struct upnp_event_notify {
struct event ev;
LIST_ENTRY(upnp_event_notify) entries;
enum { EConnecting,
ESending,
EWaitingForResponse,
EFinished,
EError } state;
struct subscriber * sub;
char * buffer;
int buffersize;
int tosend;
int sent;
const char * path;
char addrstr[16];
char portstr[8];
};
/* prototypes */
static void upnp_event_create_notify(struct subscriber * sub);
static void upnp_event_process_notify(struct event *ev);
/* Subscriber list */
LIST_HEAD(listhead, subscriber) subscriberlist = { NULL };
/* notify list */
LIST_HEAD(listheadnotif, upnp_event_notify) notifylist = { NULL };
#define MAX_SUBSCRIBERS 500
static uint16_t nsubscribers = 0;
/* create a new subscriber */
static struct subscriber *
newSubscriber(const char * eventurl, const char * callback, int callbacklen)
{
struct subscriber * tmp;
if(!eventurl || !callback || !callbacklen)
return NULL;
tmp = calloc(1, sizeof(struct subscriber)+callbacklen+1);
if(strcmp(eventurl, CONTENTDIRECTORY_EVENTURL)==0)
tmp->service = EContentDirectory;
else if(strcmp(eventurl, CONNECTIONMGR_EVENTURL)==0)
tmp->service = EConnectionManager;
else if(strcmp(eventurl, X_MS_MEDIARECEIVERREGISTRAR_EVENTURL)==0)
tmp->service = EMSMediaReceiverRegistrar;
else {
free(tmp);
return NULL;
}
memcpy(tmp->callback, callback, callbacklen);
tmp->callback[callbacklen] = '\0';
/* make a dummy uuid */
strncpyt(tmp->uuid, uuidvalue, sizeof(tmp->uuid));
if( get_uuid_string(tmp->uuid+5) != 0 )
{
tmp->uuid[sizeof(tmp->uuid)-1] = '\0';
snprintf(tmp->uuid+37, 5, "%04lx", random() & 0xffff);
}
return tmp;
}
/* creates a new subscriber and adds it to the subscriber list
* also initiate 1st notify */
const char *
upnpevents_addSubscriber(const char * eventurl,
const char * callback, int callbacklen,
int timeout)
{
struct subscriber * tmp;
DPRINTF(E_DEBUG, L_HTTP, "addSubscriber(%s, %.*s, %d)\n",
eventurl, callbacklen, callback, timeout);
if (nsubscribers >= MAX_SUBSCRIBERS)
return NULL;
tmp = newSubscriber(eventurl, callback, callbacklen);
if(!tmp)
return NULL;
if(timeout)
tmp->timeout = time(NULL) + timeout;
LIST_INSERT_HEAD(&subscriberlist, tmp, entries);
nsubscribers++;
upnp_event_create_notify(tmp);
return tmp->uuid;
}
/* renew a subscription (update the timeout) */
int
renewSubscription(const char * sid, int sidlen, int timeout)
{
struct subscriber * sub;
for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
if(memcmp(sid, sub->uuid, 41) == 0) {
sub->timeout = (timeout ? time(NULL) + timeout : 0);
return 0;
}
}
return -1;
}
int
upnpevents_removeSubscriber(const char * sid, int sidlen)
{
struct subscriber * sub;
if(!sid)
return -1;
DPRINTF(E_DEBUG, L_HTTP, "removeSubscriber(%.*s)\n",
sidlen, sid);
for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
if(memcmp(sid, sub->uuid, 41) == 0) {
if(sub->notify) {
sub->notify->sub = NULL;
}
LIST_REMOVE(sub, entries);
nsubscribers--;
free(sub);
return 0;
}
}
return -1;
}
void
upnpevents_removeSubscribers(void)
{
struct subscriber * sub;
for(sub = subscriberlist.lh_first; sub != NULL; sub = subscriberlist.lh_first) {
upnpevents_removeSubscriber(sub->uuid, sizeof(sub->uuid));
}
}
/* notifies all subscribers of a SystemUpdateID change */
void
upnp_event_var_change_notify(enum subscriber_service_enum service)
{
struct subscriber * sub;
for(sub = subscriberlist.lh_first; sub != NULL; sub = sub->entries.le_next) {
if(sub->service == service && sub->notify == NULL)
upnp_event_create_notify(sub);
}
}
/* create and add the notify object to the list, start connecting */
static void
upnp_event_create_notify(struct subscriber *sub)
{
struct upnp_event_notify * obj;
int flags, s, i;
const char *p;
unsigned short port;
struct sockaddr_in addr;
assert(sub);
obj = calloc(1, sizeof(struct upnp_event_notify));
if(!obj) {
DPRINTF(E_ERROR, L_HTTP, "%s: calloc(): %s\n", "upnp_event_create_notify", strerror(errno));
return;
}
obj->sub = sub;
s = socket(PF_INET, SOCK_STREAM, 0);
if(s < 0) {
DPRINTF(E_ERROR, L_HTTP, "%s: socket(): %s\n", "upnp_event_create_notify", strerror(errno));
goto error;
}
if((flags = fcntl(s, F_GETFL, 0)) < 0) {
DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_GETFL..): %s\n",
"upnp_event_create_notify", strerror(errno));
goto error;
}
if(fcntl(s, F_SETFL, flags | O_NONBLOCK) < 0) {
DPRINTF(E_ERROR, L_HTTP, "%s: fcntl(..F_SETFL..): %s\n",
"upnp_event_create_notify", strerror(errno));
goto error;
}
if(sub)
sub->notify = obj;
LIST_INSERT_HEAD(&notifylist, obj, entries);
memset(&addr, 0, sizeof(addr));
i = 0;
p = obj->sub->callback;
p += 7; /* http:// */
while(*p != '/' && *p != ':' && i < (sizeof(obj->addrstr)-1))
obj->addrstr[i++] = *(p++);
obj->addrstr[i] = '\0';
if(*p == ':') {
obj->portstr[0] = *p;
i = 1;
p++;
port = (unsigned short)atoi(p);
while(*p != '/' && *p != '\0') {
if(i<7) obj->portstr[i++] = *p;
p++;
}
obj->portstr[i] = 0;
} else {
port = 80;
obj->portstr[0] = '\0';
}
if( *p )
obj->path = p;
else
obj->path = "/";
addr.sin_family = AF_INET;
inet_aton(obj->addrstr, &addr.sin_addr);
addr.sin_port = htons(port);
DPRINTF(E_DEBUG, L_HTTP, "%s: '%s' %hu '%s'\n", "upnp_event_notify_connect",
obj->addrstr, port, obj->path);
obj->state = EConnecting;
if(connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
if(errno != EINPROGRESS && errno != EWOULDBLOCK) {
DPRINTF(E_ERROR, L_HTTP, "%s: connect(): %s\n", "upnp_event_notify_connect", strerror(errno));
obj->state = EError;
}
} else {
obj->ev = (struct event ){ .fd = s, .rdwr = EVENT_WRITE,
.process = upnp_event_process_notify, .data = obj };
event_module.add(&obj->ev);
}
return;
error:
if(s >= 0)
close(s);
free(obj);
}
static void upnp_event_prepare(struct upnp_event_notify * obj)
{
static const char notifymsg[] =
"NOTIFY %s HTTP/1.1\r\n"
"Host: %s%s\r\n"
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
"Content-Length: %d\r\n"
"NT: upnp:event\r\n"
"NTS: upnp:propchange\r\n"
"SID: %s\r\n"
"SEQ: %u\r\n"
"Connection: close\r\n"
"Cache-Control: no-cache\r\n"
"\r\n"
"%.*s\r\n";
char * xml;
int l;
assert(obj->sub);
switch(obj->sub->service) {
case EContentDirectory:
xml = getVarsContentDirectory(&l);
break;
case EConnectionManager:
xml = getVarsConnectionManager(&l);
break;
case EMSMediaReceiverRegistrar:
xml = getVarsX_MS_MediaReceiverRegistrar(&l);
break;
default:
xml = NULL;
l = 0;
}
obj->tosend = asprintf(&(obj->buffer), notifymsg,
obj->path, obj->addrstr, obj->portstr, l+2,
obj->sub->uuid, obj->sub->seq,
l, xml);
obj->buffersize = obj->tosend;
free(xml);
DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event response:\n%s\n", obj->buffer);
obj->state = ESending;
}
static void upnp_event_send(struct upnp_event_notify * obj)
{
int i;
//DEBUG DPRINTF(E_DEBUG, L_HTTP, "Sending UPnP Event:\n%s", obj->buffer+obj->sent);
while( obj->sent < obj->tosend ) {
i = send(obj->ev.fd, obj->buffer + obj->sent, obj->tosend - obj->sent, 0);
if(i<0) {
DPRINTF(E_WARN, L_HTTP, "%s: send(): %s\n", "upnp_event_send", strerror(errno));
obj->state = EError;
event_module.del(&obj->ev);
return;
}
obj->sent += i;
}
if(obj->sent == obj->tosend) {
obj->state = EWaitingForResponse;
event_module.del(&obj->ev);
obj->ev.rdwr = EVENT_READ;
event_module.add(&obj->ev);
}
}
static void upnp_event_recv(struct upnp_event_notify * obj)
{
int n;
n = recv(obj->ev.fd, obj->buffer, obj->buffersize, 0);
if(n<0) {
DPRINTF(E_ERROR, L_HTTP, "%s: recv(): %s\n", "upnp_event_recv", strerror(errno));
obj->state = EError;
event_module.del(&obj->ev);
return;
}
DPRINTF(E_DEBUG, L_HTTP, "%s: (%dbytes) %.*s\n", "upnp_event_recv",
n, n, obj->buffer);
obj->state = EFinished;
event_module.del(&obj->ev);
if(obj->sub)
{
obj->sub->seq++;
if (!obj->sub->seq)
obj->sub->seq++;
}
}
static void
upnp_event_process_notify(struct event *ev)
{
struct upnp_event_notify *obj = ev->data;
switch(obj->state) {
case EConnecting:
/* now connected or failed to connect */
upnp_event_prepare(obj);
upnp_event_send(obj);
break;
case ESending:
upnp_event_send(obj);
break;
case EWaitingForResponse:
upnp_event_recv(obj);
break;
case EFinished:
close(obj->ev.fd);
obj->ev.fd = -1;
break;
default:
DPRINTF(E_ERROR, L_HTTP, "upnp_event_process_notify: unknown state\n");
}
}
void upnpevents_gc(void)
{
struct upnp_event_notify * obj;
struct upnp_event_notify * next;
struct subscriber * sub;
struct subscriber * subnext;
time_t curtime;
obj = notifylist.lh_first;
while(obj != NULL) {
next = obj->entries.le_next;
if(obj->state == EError || obj->state == EFinished) {
if(obj->ev.fd >= 0) {
close(obj->ev.fd);
}
if(obj->sub)
obj->sub->notify = NULL;
/* remove also the subscriber from the list if there was an error */
if(obj->state == EError && obj->sub) {
LIST_REMOVE(obj->sub, entries);
nsubscribers--;
free(obj->sub);
}
free(obj->buffer);
LIST_REMOVE(obj, entries);
free(obj);
}
obj = next;
}
/* remove timed-out subscribers */
curtime = time(NULL);
for(sub = subscriberlist.lh_first; sub != NULL; ) {
subnext = sub->entries.le_next;
if(sub->timeout && curtime > sub->timeout && sub->notify == NULL) {
LIST_REMOVE(sub, entries);
nsubscribers--;
free(sub);
}
sub = subnext;
}
}