mirror of
https://github.com/transmission/transmission
synced 2024-12-25 17:17:31 +00:00
1367 lines
37 KiB
C
1367 lines
37 KiB
C
/******************************************************************************
|
|
* $Id$
|
|
*
|
|
* Copyright (c) 2006 Transmission authors and contributors
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*****************************************************************************/
|
|
|
|
#include "transmission.h"
|
|
|
|
#define SSDP_ADDR "239.255.255.250"
|
|
#define SSDP_PORT 1900
|
|
#define SSDP_TYPE "upnp:rootdevice"
|
|
#define SSDP_SUBTYPE "ssdp:alive"
|
|
#define SSDP_FIRST_DELAY 3750 /* 3 3/4 seconds */
|
|
#define SSDP_MAX_DELAY 1800000 /* 30 minutes */
|
|
#define UPNP_SERVICE_TYPE "urn:schemas-upnp-org:service:WANIPConnection:1"
|
|
#define SOAP_ENVELOPE "http://schemas.xmlsoap.org/soap/envelope/"
|
|
#define LOOP_DETECT_THRESHOLD 10 /* error on 10 add/get/del state changes */
|
|
#define MAPPING_CHECK_INTERVAL 900000 /* 15 minutes */
|
|
#define HTTP_REQUEST_INTERVAL 500 /* half a second */
|
|
#define SOAP_METHOD_NOT_ALLOWED 405
|
|
#define IGD_GENERIC_ERROR 500
|
|
#define IGD_GENERIC_FAILED 501
|
|
#define IGD_NO_MAPPING_EXISTS 714
|
|
#define IGD_ADD_CONFLICT 718
|
|
#define IGD_NO_DYNAMIC_MAPPING 725
|
|
|
|
typedef struct tr_upnp_action_s
|
|
{
|
|
char * name;
|
|
char * action;
|
|
int len;
|
|
struct { char * name; char * var; char dir; } * args;
|
|
} tr_upnp_action_t;
|
|
|
|
typedef struct tr_upnp_device_s
|
|
{
|
|
char * id;
|
|
char * host;
|
|
char * root;
|
|
int port;
|
|
char * soap;
|
|
char * scpd;
|
|
int mappedport;
|
|
char * myaddr;
|
|
#define UPNPDEV_STATE_ROOT 1
|
|
#define UPNPDEV_STATE_SCPD 2
|
|
#define UPNPDEV_STATE_READY 3
|
|
#define UPNPDEV_STATE_ADD 4
|
|
#define UPNPDEV_STATE_GET 5
|
|
#define UPNPDEV_STATE_DEL 6
|
|
#define UPNPDEV_STATE_MAPPED 7
|
|
#define UPNPDEV_STATE_ERROR 8
|
|
uint8_t state;
|
|
uint8_t looping;
|
|
uint64_t lastrequest;
|
|
uint64_t lastcheck;
|
|
unsigned int soapretry : 1;
|
|
tr_http_t * http;
|
|
tr_upnp_action_t getcmd;
|
|
tr_upnp_action_t addcmd;
|
|
tr_upnp_action_t delcmd;
|
|
struct tr_upnp_device_s * next;
|
|
} tr_upnp_device_t;
|
|
|
|
struct tr_upnp_s
|
|
{
|
|
int port;
|
|
int infd;
|
|
int outfd;
|
|
uint64_t lastdiscover;
|
|
uint64_t lastdelay;
|
|
unsigned int active : 1;
|
|
unsigned int discovering : 1;
|
|
tr_upnp_device_t * devices;
|
|
tr_lock_t lock;
|
|
};
|
|
|
|
static int
|
|
sendSSDP( int fd );
|
|
static int
|
|
mcastStart();
|
|
static void
|
|
killSock( int * sock );
|
|
static void
|
|
killHttp( tr_http_t ** http );
|
|
static int
|
|
watchSSDP( tr_upnp_device_t ** devices, int fd );
|
|
static tr_tristate_t
|
|
recvSSDP( int fd, char * buf, int * len );
|
|
static int
|
|
parseSSDP( char * buf, int len, tr_http_header_t * headers );
|
|
static void
|
|
deviceAdd( tr_upnp_device_t ** first, const char * id, int idLen,
|
|
const char * url, int urlLen );
|
|
static void
|
|
deviceRemove( tr_upnp_device_t ** prevptr );
|
|
static int
|
|
deviceStop( tr_upnp_device_t * dev );
|
|
static int
|
|
devicePulse( tr_upnp_device_t * dev, int port );
|
|
static int
|
|
devicePulseHttp( tr_upnp_device_t * dev,
|
|
const char ** body, int * len );
|
|
static tr_http_t *
|
|
devicePulseGetHttp( tr_upnp_device_t * dev );
|
|
static int
|
|
parseRoot( const char *buf, int len, char ** soap, char ** scpd );
|
|
static void
|
|
addUrlbase( const char * base, char ** path );
|
|
static int
|
|
parseScpd( const char *buf, int len, tr_upnp_action_t * getcmd,
|
|
tr_upnp_action_t * addcmd, tr_upnp_action_t * delcmd );
|
|
static int
|
|
parseScpdArgs( const char * buf, const char * end,
|
|
tr_upnp_action_t * action, char dir );
|
|
static int
|
|
parseMapping( tr_upnp_device_t * dev, const char * buf, int len );
|
|
static char *
|
|
joinstrs( const char *, const char *, const char * );
|
|
static tr_http_t *
|
|
soapRequest( int retry, const char * host, int port, const char * path,
|
|
tr_upnp_action_t * action, ... );
|
|
static void
|
|
actionSetup( tr_upnp_action_t * action, const char * name, int prealloc );
|
|
static void
|
|
actionFree( tr_upnp_action_t * action );
|
|
static int
|
|
actionAdd( tr_upnp_action_t * action, char * name, char * var,
|
|
char dir );
|
|
#define actionLookupVar( act, nam, len, dir ) \
|
|
( actionLookup( (act), (nam), (len), (dir), 0 ) )
|
|
#define actionLookupName( act, var, len, dir ) \
|
|
( actionLookup( (act), (var), (len), (dir), 1 ) )
|
|
static const char *
|
|
actionLookup( tr_upnp_action_t * action, const char * key, int len,
|
|
char dir, int getname );
|
|
|
|
tr_upnp_t *
|
|
tr_upnpInit()
|
|
{
|
|
tr_upnp_t * upnp;
|
|
|
|
upnp = calloc( 1, sizeof( *upnp ) );
|
|
if( NULL == upnp )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
upnp->infd = -1;
|
|
upnp->outfd = -1;
|
|
|
|
tr_lockInit( &upnp->lock );
|
|
|
|
return upnp;
|
|
}
|
|
|
|
void
|
|
tr_upnpStart( tr_upnp_t * upnp )
|
|
{
|
|
tr_lockLock( &upnp->lock );
|
|
|
|
if( !upnp->active )
|
|
{
|
|
tr_inf( "starting upnp" );
|
|
upnp->active = 1;
|
|
upnp->discovering = 1;
|
|
upnp->infd = mcastStart();
|
|
upnp->lastdiscover = 0;
|
|
upnp->lastdelay = SSDP_FIRST_DELAY / 2;
|
|
}
|
|
|
|
tr_lockUnlock( &upnp->lock );
|
|
}
|
|
|
|
void
|
|
tr_upnpStop( tr_upnp_t * upnp )
|
|
{
|
|
tr_lockLock( &upnp->lock );
|
|
|
|
if( upnp->active )
|
|
{
|
|
tr_inf( "stopping upnp" );
|
|
upnp->active = 0;
|
|
killSock( &upnp->infd );
|
|
killSock( &upnp->outfd );
|
|
}
|
|
|
|
tr_lockUnlock( &upnp->lock );
|
|
}
|
|
|
|
int
|
|
tr_upnpStatus( tr_upnp_t * upnp )
|
|
{
|
|
tr_upnp_device_t * ii;
|
|
int ret;
|
|
|
|
tr_lockLock( &upnp->lock );
|
|
|
|
if( !upnp->active )
|
|
{
|
|
ret = ( NULL == upnp->devices ?
|
|
TR_NAT_TRAVERSAL_DISABLED : TR_NAT_TRAVERSAL_UNMAPPING );
|
|
}
|
|
else if( NULL == upnp->devices )
|
|
{
|
|
ret = TR_NAT_TRAVERSAL_NOTFOUND;
|
|
}
|
|
else
|
|
{
|
|
ret = TR_NAT_TRAVERSAL_MAPPING;
|
|
for( ii = upnp->devices; NULL != ii; ii = ii->next )
|
|
{
|
|
if( UPNPDEV_STATE_ERROR == ii->state )
|
|
{
|
|
ret = TR_NAT_TRAVERSAL_ERROR;
|
|
}
|
|
else if( 0 < ii->mappedport )
|
|
{
|
|
ret = TR_NAT_TRAVERSAL_MAPPED;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
tr_lockUnlock( &upnp->lock );
|
|
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
tr_upnpForwardPort( tr_upnp_t * upnp, int port )
|
|
{
|
|
tr_lockLock( &upnp->lock );
|
|
tr_dbg( "upnp port changed from %i to %i", upnp->port, port );
|
|
upnp->port = port;
|
|
tr_lockUnlock( &upnp->lock );
|
|
}
|
|
|
|
void
|
|
tr_upnpClose( tr_upnp_t * upnp )
|
|
{
|
|
tr_upnpStop( upnp );
|
|
|
|
tr_lockLock( &upnp->lock );
|
|
while( NULL != upnp->devices )
|
|
{
|
|
deviceRemove( &upnp->devices );
|
|
}
|
|
|
|
tr_lockClose( &upnp->lock );
|
|
free( upnp );
|
|
}
|
|
|
|
void
|
|
tr_upnpPulse( tr_upnp_t * upnp )
|
|
{
|
|
tr_upnp_device_t ** ii;
|
|
|
|
tr_lockLock( &upnp->lock );
|
|
|
|
if( upnp->active )
|
|
{
|
|
/* pulse on all known devices */
|
|
upnp->discovering = 1;
|
|
for( ii = &upnp->devices; NULL != *ii; ii = &(*ii)->next )
|
|
{
|
|
if( devicePulse( *ii, upnp->port ) )
|
|
{
|
|
upnp->discovering = 0;
|
|
}
|
|
}
|
|
|
|
/* send an SSDP discover message */
|
|
if( upnp->discovering &&
|
|
upnp->lastdelay + upnp->lastdiscover < tr_date() )
|
|
{
|
|
upnp->outfd = sendSSDP( upnp->outfd );
|
|
upnp->lastdiscover = tr_date();
|
|
upnp->lastdelay = MIN( upnp->lastdelay * 2, SSDP_MAX_DELAY );
|
|
}
|
|
|
|
/* try to receive SSDP messages */
|
|
watchSSDP( &upnp->devices, upnp->infd );
|
|
if( watchSSDP( &upnp->devices, upnp->outfd ) )
|
|
{
|
|
killSock( &upnp->outfd );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* delete all mappings then delete devices */
|
|
ii = &upnp->devices;
|
|
while( NULL != *ii )
|
|
{
|
|
if( deviceStop( *ii ) )
|
|
{
|
|
deviceRemove( ii );
|
|
}
|
|
else
|
|
{
|
|
devicePulse( *ii, 0 );
|
|
ii = &(*ii)->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
tr_lockUnlock( &upnp->lock );
|
|
}
|
|
|
|
static int
|
|
sendSSDP( int fd )
|
|
{
|
|
char buf[102];
|
|
int len;
|
|
struct sockaddr_in sin;
|
|
|
|
if( 0 > fd )
|
|
{
|
|
fd = tr_netBindUDP( 0 );
|
|
if( 0 > fd )
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
tr_dbg( "sending upnp ssdp discover message" );
|
|
|
|
len = snprintf( buf, sizeof( buf ),
|
|
"M-SEARCH * HTTP/1.1\r\n"
|
|
"Host: %s:%i\r\n"
|
|
"Man: \"ssdp:discover\"\r\n"
|
|
"ST: %s\r\n"
|
|
"MX: 3\r\n"
|
|
"\r\n",
|
|
SSDP_ADDR, SSDP_PORT, SSDP_TYPE );
|
|
|
|
/* if this assertion ever fails then just increase the size of buf */
|
|
assert( (int) sizeof( buf ) > len );
|
|
|
|
memset( &sin, 0, sizeof( sin ) );
|
|
sin.sin_family = AF_INET;
|
|
sin.sin_addr.s_addr = inet_addr( SSDP_ADDR );
|
|
sin.sin_port = htons( SSDP_PORT );
|
|
|
|
if( 0 > sendto( fd, buf, len, 0,
|
|
(struct sockaddr*) &sin, sizeof( sin ) ) )
|
|
{
|
|
if( EAGAIN != errno )
|
|
{
|
|
tr_err( "Could not send SSDP discover message (%s)",
|
|
strerror( errno ) );
|
|
}
|
|
killSock( &fd );
|
|
return -1;
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
static int
|
|
mcastStart()
|
|
{
|
|
int fd;
|
|
struct in_addr addr;
|
|
|
|
addr.s_addr = inet_addr( SSDP_ADDR );
|
|
fd = tr_netMcastOpen( SSDP_PORT, addr );
|
|
if( 0 > fd )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
static void
|
|
killSock( int * sock )
|
|
{
|
|
if( 0 <= *sock )
|
|
{
|
|
tr_netClose( *sock );
|
|
*sock = -1;
|
|
}
|
|
}
|
|
|
|
static void
|
|
killHttp( tr_http_t ** http )
|
|
{
|
|
tr_httpClose( *http );
|
|
*http = NULL;
|
|
}
|
|
|
|
static int
|
|
watchSSDP( tr_upnp_device_t ** devices, int fd )
|
|
{
|
|
/* XXX what if we get a huge SSDP packet? */
|
|
char buf[512];
|
|
int len;
|
|
tr_http_header_t hdr[] = {
|
|
/* first one must be type and second must be subtype */
|
|
{ NULL, NULL, 0 },
|
|
{ "NTS", NULL, 0 },
|
|
/* XXX should probably look at this
|
|
{ "Cache-control", NULL, 0 }, */
|
|
{ "Location", NULL, 0 },
|
|
{ "USN", NULL, 0 },
|
|
{ NULL, NULL, 0 }
|
|
};
|
|
enum { OFF_TYPE = 0, OFF_SUBTYPE, OFF_LOC, OFF_ID };
|
|
int ret;
|
|
|
|
if( 0 > fd )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
ret = 0;
|
|
for(;;)
|
|
{
|
|
len = sizeof( buf );
|
|
switch( recvSSDP( fd, buf, &len ) )
|
|
{
|
|
case TR_NET_WAIT:
|
|
return ret;
|
|
case TR_NET_ERROR:
|
|
return 1;
|
|
case TR_NET_OK:
|
|
ret = 1;
|
|
if( parseSSDP( buf, len, hdr ) &&
|
|
NULL != hdr[OFF_LOC].data &&
|
|
NULL != hdr[OFF_ID].data )
|
|
{
|
|
deviceAdd( devices, hdr[OFF_ID].data, hdr[OFF_ID].len,
|
|
hdr[OFF_LOC].data, hdr[OFF_LOC].len );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static tr_tristate_t
|
|
recvSSDP( int fd, char * buf, int * len )
|
|
{
|
|
if( 0 > fd )
|
|
{
|
|
return TR_NET_ERROR;
|
|
}
|
|
|
|
*len = tr_netRecv( fd, ( uint8_t * ) buf, *len );
|
|
if( TR_NET_BLOCK & *len )
|
|
{
|
|
return TR_NET_WAIT;
|
|
}
|
|
else if( TR_NET_CLOSE & *len )
|
|
{
|
|
tr_err( "Could not receive SSDP message (%s)", strerror( errno ) );
|
|
return TR_NET_ERROR;
|
|
}
|
|
else
|
|
{
|
|
return TR_NET_OK;
|
|
}
|
|
}
|
|
|
|
static int
|
|
parseSSDP( char * buf, int len, tr_http_header_t * hdr )
|
|
{
|
|
char *method, *uri, *body;
|
|
int code;
|
|
|
|
body = NULL;
|
|
/* check for an HTTP NOTIFY request */
|
|
if( 0 <= tr_httpRequestType( buf, len, &method, &uri ) )
|
|
{
|
|
if( 0 == tr_strcasecmp( method, "NOTIFY" ) && 0 == strcmp( uri, "*" ) )
|
|
{
|
|
hdr[0].name = "NT";
|
|
body = tr_httpParse( buf, len, hdr );
|
|
if( NULL == hdr[1].name ||
|
|
0 != tr_strncasecmp( SSDP_SUBTYPE, hdr[1].data, hdr[1].len ) )
|
|
{
|
|
body = NULL;
|
|
}
|
|
else
|
|
{
|
|
tr_dbg( "found upnp ssdp notify request" );
|
|
}
|
|
}
|
|
free( method );
|
|
free( uri );
|
|
}
|
|
else
|
|
{
|
|
/* check for a response to our HTTP M-SEARCH request */
|
|
code = tr_httpResponseCode( buf, len );
|
|
if( TR_HTTP_STATUS_OK( code ) )
|
|
{
|
|
hdr[0].name = "ST";
|
|
body = tr_httpParse( buf, len, hdr );
|
|
if( NULL != body )
|
|
{
|
|
tr_dbg( "found upnp ssdp m-search response" );
|
|
}
|
|
}
|
|
}
|
|
|
|
/* did we find enough information to be useful? */
|
|
if( NULL != body )
|
|
{
|
|
/* the first header is the type */
|
|
if( NULL != hdr[0].data &&
|
|
0 == tr_strncasecmp( SSDP_TYPE, hdr[0].data, hdr[0].len ) )
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
deviceAdd( tr_upnp_device_t ** first, const char * id, int idLen,
|
|
const char * url, int urlLen )
|
|
{
|
|
tr_upnp_device_t * ii;
|
|
|
|
for( ii = *first; NULL != ii; ii = ii->next )
|
|
{
|
|
if( 0 == tr_strncasecmp( ii->id, id, idLen ) )
|
|
{
|
|
/* this device may have gone away and came back, recheck it */
|
|
ii->lastcheck = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ii = malloc( sizeof( *ii ) );
|
|
if( NULL == ii )
|
|
{
|
|
return;
|
|
}
|
|
memset( ii, 0, sizeof( *ii ) );
|
|
if( tr_httpParseUrl( url, urlLen, &ii->host, &ii->port, &ii->root ) )
|
|
{
|
|
tr_err( "Invalid HTTP URL from UPnP" );
|
|
free( ii );
|
|
return;
|
|
}
|
|
ii->id = tr_dupstr( id, idLen );
|
|
ii->state = UPNPDEV_STATE_ROOT;
|
|
actionSetup( &ii->getcmd, "GetSpecificPortMappingEntry", 8 );
|
|
actionSetup( &ii->addcmd, "AddPortMapping", 8 );
|
|
actionSetup( &ii->delcmd, "DeletePortMapping", 3 );
|
|
ii->next = *first;
|
|
*first = ii;
|
|
|
|
tr_inf( "new upnp device %s, port %i, path %s",
|
|
ii->host, ii->port, ii->root );
|
|
}
|
|
|
|
static void
|
|
deviceRemove( tr_upnp_device_t ** prevptr )
|
|
{
|
|
tr_upnp_device_t * dead;
|
|
|
|
dead = *prevptr;
|
|
*prevptr = dead->next;
|
|
|
|
tr_inf( "forgetting upnp device %s", dead->host );
|
|
|
|
free( dead->id );
|
|
free( dead->host );
|
|
free( dead->root );
|
|
free( dead->soap );
|
|
free( dead->scpd );
|
|
free( dead->myaddr );
|
|
if( NULL != dead->http )
|
|
{
|
|
killHttp( &dead->http );
|
|
}
|
|
actionFree( &dead->getcmd );
|
|
actionFree( &dead->addcmd );
|
|
actionFree( &dead->delcmd );
|
|
free( dead );
|
|
}
|
|
|
|
static int
|
|
deviceStop( tr_upnp_device_t * dev )
|
|
{
|
|
switch( dev->state )
|
|
{
|
|
case UPNPDEV_STATE_READY:
|
|
case UPNPDEV_STATE_ERROR:
|
|
return 1;
|
|
case UPNPDEV_STATE_MAPPED:
|
|
tr_dbg( "upnp device %s: stopping upnp, state mapped -> delete",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_DEL;
|
|
return 0;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int
|
|
devicePulse( tr_upnp_device_t * dev, int port )
|
|
{
|
|
const char * body;
|
|
int len, code;
|
|
uint8_t laststate;
|
|
|
|
switch( dev->state )
|
|
{
|
|
case UPNPDEV_STATE_READY:
|
|
if( 0 < port )
|
|
{
|
|
tr_dbg( "upnp device %s: want mapping, state ready -> get",
|
|
dev->host );
|
|
dev->mappedport = port;
|
|
dev->state = UPNPDEV_STATE_GET;
|
|
break;
|
|
}
|
|
return 1;
|
|
case UPNPDEV_STATE_MAPPED:
|
|
if( port != dev->mappedport )
|
|
{
|
|
tr_dbg( "upnp device %s: change mapping, "
|
|
"state mapped -> delete", dev->host );
|
|
dev->state = UPNPDEV_STATE_DEL;
|
|
break;
|
|
}
|
|
if( tr_date() > dev->lastcheck + MAPPING_CHECK_INTERVAL )
|
|
{
|
|
tr_dbg( "upnp device %s: check mapping, "
|
|
"state mapped -> get", dev->host );
|
|
dev->state = UPNPDEV_STATE_GET;
|
|
}
|
|
return 1;
|
|
case UPNPDEV_STATE_ERROR:
|
|
return 0;
|
|
}
|
|
|
|
/* gcc can be pretty annoying about it's warnings sometimes */
|
|
len = 0;
|
|
body = NULL;
|
|
|
|
code = devicePulseHttp( dev, &body, &len );
|
|
if( 0 > code )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if( LOOP_DETECT_THRESHOLD <= dev->looping )
|
|
{
|
|
tr_dbg( "upnp device %s: loop detected, state %hhu -> error",
|
|
dev->host, dev->state );
|
|
dev->state = UPNPDEV_STATE_ERROR;
|
|
dev->looping = 0;
|
|
killHttp( &dev->http );
|
|
return 1;
|
|
}
|
|
|
|
laststate = dev->state;
|
|
dev->state = UPNPDEV_STATE_ERROR;
|
|
switch( laststate )
|
|
{
|
|
case UPNPDEV_STATE_ROOT:
|
|
if( !TR_HTTP_STATUS_OK( code ) )
|
|
{
|
|
tr_dbg( "upnp device %s: fetch root failed with http code %i",
|
|
dev->host, code );
|
|
}
|
|
else if( parseRoot( body, len, &dev->soap, &dev->scpd ) )
|
|
{
|
|
tr_dbg( "upnp device %s: parse root failed", dev->host );
|
|
}
|
|
else
|
|
{
|
|
tr_dbg( "upnp device %s: found scpd \"%s\" and soap \"%s\"",
|
|
dev->root, dev->scpd, dev->soap );
|
|
tr_dbg( "upnp device %s: parsed root, state root -> scpd",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_SCPD;
|
|
}
|
|
break;
|
|
|
|
case UPNPDEV_STATE_SCPD:
|
|
if( !TR_HTTP_STATUS_OK( code ) )
|
|
{
|
|
tr_dbg( "upnp device %s: fetch scpd failed with http code %i",
|
|
dev->host, code );
|
|
}
|
|
else if( parseScpd( body, len, &dev->getcmd,
|
|
&dev->addcmd, &dev->delcmd ) )
|
|
{
|
|
tr_dbg( "upnp device %s: parse scpd failed", dev->host );
|
|
}
|
|
else
|
|
{
|
|
tr_dbg( "upnp device %s: parsed scpd, state scpd -> ready",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_READY;
|
|
dev->looping = 0;
|
|
}
|
|
break;
|
|
|
|
case UPNPDEV_STATE_ADD:
|
|
dev->looping++;
|
|
if( IGD_ADD_CONFLICT == code )
|
|
{
|
|
tr_dbg( "upnp device %s: add conflict, state add -> delete",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_DEL;
|
|
}
|
|
else if( TR_HTTP_STATUS_OK( code ) ||
|
|
IGD_GENERIC_ERROR == code || IGD_GENERIC_FAILED == code )
|
|
{
|
|
tr_dbg( "upnp device %s: add attempt, state add -> get",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_GET;
|
|
}
|
|
else
|
|
{
|
|
tr_dbg( "upnp device %s: add failed with http code %i",
|
|
dev->host, code );
|
|
}
|
|
break;
|
|
|
|
case UPNPDEV_STATE_GET:
|
|
dev->looping++;
|
|
if( TR_HTTP_STATUS_OK( code ) )
|
|
{
|
|
switch( parseMapping( dev, body, len ) )
|
|
{
|
|
case -1:
|
|
break;
|
|
case 0:
|
|
tr_dbg( "upnp device %s: invalid mapping, "
|
|
"state get -> delete", dev->host );
|
|
dev->state = UPNPDEV_STATE_DEL;
|
|
break;
|
|
case 1:
|
|
tr_dbg( "upnp device %s: good mapping, "
|
|
"state get -> mapped", dev->host );
|
|
dev->state = UPNPDEV_STATE_MAPPED;
|
|
dev->looping = 0;
|
|
dev->lastcheck = tr_date();
|
|
tr_inf( "upnp successful for port %i",
|
|
dev->mappedport );
|
|
break;
|
|
default:
|
|
assert( 0 );
|
|
break;
|
|
}
|
|
}
|
|
else if( IGD_NO_MAPPING_EXISTS == code ||
|
|
IGD_GENERIC_ERROR == code || IGD_GENERIC_FAILED == code )
|
|
{
|
|
tr_dbg( "upnp device %s: no mapping, state get -> add",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_ADD;
|
|
}
|
|
else
|
|
{
|
|
tr_dbg( "upnp device %s: get failed with http code %i",
|
|
dev->host, code );
|
|
}
|
|
break;
|
|
|
|
case UPNPDEV_STATE_DEL:
|
|
dev->looping++;
|
|
if( TR_HTTP_STATUS_OK( code ) || IGD_NO_MAPPING_EXISTS == code ||
|
|
IGD_GENERIC_ERROR == code || IGD_GENERIC_FAILED == code )
|
|
{
|
|
tr_dbg( "upnp device %s: deleted, state delete -> ready",
|
|
dev->host );
|
|
dev->state = UPNPDEV_STATE_READY;
|
|
dev->looping = 0;
|
|
}
|
|
else
|
|
{
|
|
tr_dbg( "upnp device %s: del failed with http code %i",
|
|
dev->host, code );
|
|
}
|
|
break;
|
|
|
|
default:
|
|
assert( 0 );
|
|
break;
|
|
}
|
|
|
|
dev->lastrequest = tr_date();
|
|
killHttp( &dev->http );
|
|
|
|
if( UPNPDEV_STATE_ERROR == dev->state )
|
|
{
|
|
tr_dbg( "upnp device %s: error, state %hhu -> error",
|
|
dev->host, laststate );
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static tr_http_t *
|
|
makeHttp( int method, const char * host, int port, const char * path )
|
|
{
|
|
if( tr_httpIsUrl( path, -1 ) )
|
|
{
|
|
return tr_httpClientUrl( method, "%s", path );
|
|
}
|
|
else
|
|
{
|
|
return tr_httpClient( method, host, port, "%s", path );
|
|
}
|
|
}
|
|
|
|
static tr_http_t *
|
|
devicePulseGetHttp( tr_upnp_device_t * dev )
|
|
{
|
|
tr_http_t * ret;
|
|
char numstr[6];
|
|
|
|
ret = NULL;
|
|
switch( dev->state )
|
|
{
|
|
case UPNPDEV_STATE_ROOT:
|
|
if( !dev->soapretry )
|
|
{
|
|
ret = makeHttp( TR_HTTP_GET, dev->host, dev->port, dev->root );
|
|
}
|
|
break;
|
|
case UPNPDEV_STATE_SCPD:
|
|
if( !dev->soapretry )
|
|
{
|
|
ret = makeHttp( TR_HTTP_GET, dev->host, dev->port, dev->scpd );
|
|
}
|
|
break;
|
|
case UPNPDEV_STATE_ADD:
|
|
if( NULL == dev->myaddr )
|
|
{
|
|
ret = NULL;
|
|
break;
|
|
}
|
|
snprintf( numstr, sizeof( numstr ), "%i", dev->mappedport );
|
|
ret = soapRequest( dev->soapretry, dev->host, dev->port, dev->soap,
|
|
&dev->addcmd,
|
|
"PortMappingEnabled", "1",
|
|
"PortMappingLeaseDuration", "0",
|
|
"RemoteHost", "",
|
|
"ExternalPort", numstr,
|
|
"InternalPort", numstr,
|
|
"PortMappingProtocol", "TCP",
|
|
"InternalClient", dev->myaddr,
|
|
"PortMappingDescription",
|
|
"Added by Transmission",
|
|
NULL );
|
|
break;
|
|
case UPNPDEV_STATE_GET:
|
|
snprintf( numstr, sizeof( numstr ), "%i", dev->mappedport );
|
|
ret = soapRequest( dev->soapretry, dev->host, dev->port, dev->soap,
|
|
&dev->getcmd,
|
|
"RemoteHost", "",
|
|
"ExternalPort", numstr,
|
|
"PortMappingProtocol", "TCP",
|
|
NULL );
|
|
break;
|
|
case UPNPDEV_STATE_DEL:
|
|
snprintf( numstr, sizeof( numstr ), "%i", dev->mappedport );
|
|
ret = soapRequest( dev->soapretry, dev->host, dev->port, dev->soap,
|
|
&dev->delcmd,
|
|
"RemoteHost", "",
|
|
"ExternalPort", numstr,
|
|
"PortMappingProtocol", "TCP",
|
|
NULL );
|
|
break;
|
|
default:
|
|
assert( 0 );
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
devicePulseHttp( tr_upnp_device_t * dev,
|
|
const char ** body, int * len )
|
|
{
|
|
const char * headers;
|
|
int hlen, code;
|
|
|
|
if( NULL == dev->http )
|
|
{
|
|
if( tr_date() < dev->lastrequest + HTTP_REQUEST_INTERVAL )
|
|
{
|
|
return -1;
|
|
}
|
|
dev->lastrequest = tr_date();
|
|
dev->http = devicePulseGetHttp( dev );
|
|
if( NULL == dev->http )
|
|
{
|
|
tr_dbg( "upnp device %s: http init failed, state %hhu -> error",
|
|
dev->host, dev->state );
|
|
dev->state = UPNPDEV_STATE_ERROR;
|
|
dev->soapretry = 0;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if( NULL == dev->myaddr )
|
|
{
|
|
dev->myaddr = tr_httpWhatsMyAddress( dev->http );
|
|
}
|
|
|
|
switch( tr_httpPulse( dev->http, &headers, &hlen ) )
|
|
{
|
|
case TR_NET_OK:
|
|
code = tr_httpResponseCode( headers, hlen );
|
|
if( SOAP_METHOD_NOT_ALLOWED == code && !dev->soapretry )
|
|
{
|
|
dev->soapretry = 1;
|
|
killHttp( &dev->http );
|
|
break;
|
|
}
|
|
dev->soapretry = 0;
|
|
*body = tr_httpParse( headers, hlen, NULL );
|
|
*len = ( NULL == body ? 0 : hlen - ( *body - headers ) );
|
|
return code;
|
|
case TR_NET_ERROR:
|
|
killHttp( &dev->http );
|
|
if( dev->soapretry )
|
|
{
|
|
tr_dbg( "upnp device %s: http pulse failed, state %hhu -> error",
|
|
dev->host, dev->state );
|
|
dev->state = UPNPDEV_STATE_ERROR;
|
|
dev->soapretry = 0;
|
|
}
|
|
else
|
|
{
|
|
dev->soapretry = 1;
|
|
}
|
|
break;
|
|
case TR_NET_WAIT:
|
|
break;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
parseRoot(const char *buf, int len, char ** soap, char ** scpd )
|
|
{
|
|
const char * end, * ii, * jj, * kk, * urlbase;
|
|
char * basedup;
|
|
|
|
*soap = NULL;
|
|
*scpd = NULL;
|
|
end = buf + len;
|
|
|
|
buf = tr_xmlFindTagContents( buf, end, "root" );
|
|
urlbase = tr_xmlFindTag( buf, end, "urlBase" );
|
|
urlbase = tr_xmlTagContents( urlbase, end );
|
|
buf = tr_xmlFindTagContents( buf, end, "device" );
|
|
if( tr_xmlFindTagVerifyContents( buf, end, "deviceType",
|
|
"urn:schemas-upnp-org:device:InternetGatewayDevice:1", 1 ) )
|
|
{
|
|
return 1;
|
|
}
|
|
buf = tr_xmlFindTag( buf, end, "deviceList" );
|
|
ii = tr_xmlTagContents( buf, end );
|
|
for( ; NULL != ii; ii = tr_xmlSkipTag( ii, end ) )
|
|
{
|
|
ii = tr_xmlFindTag( ii, end, "device" );
|
|
buf = tr_xmlTagContents( ii, end );
|
|
if( tr_xmlFindTagVerifyContents( buf, end, "deviceType",
|
|
"urn:schemas-upnp-org:device:WANDevice:1", 1 ) )
|
|
{
|
|
continue;
|
|
}
|
|
buf = tr_xmlFindTag( buf, end, "deviceList" );
|
|
jj = tr_xmlTagContents( buf, end );
|
|
for( ; NULL != jj; jj = tr_xmlSkipTag( jj, end ) )
|
|
{
|
|
jj = tr_xmlFindTag( jj, end, "device" );
|
|
buf = tr_xmlTagContents( jj, end );
|
|
if( tr_xmlFindTagVerifyContents( buf, end, "deviceType",
|
|
"urn:schemas-upnp-org:device:WANConnectionDevice:1", 1 ) )
|
|
{
|
|
continue;
|
|
}
|
|
buf = tr_xmlFindTag( buf, end, "serviceList" );
|
|
kk = tr_xmlTagContents( buf, end );
|
|
for( ; NULL != kk; kk = tr_xmlSkipTag( kk, end ) )
|
|
{
|
|
kk = tr_xmlFindTag( kk, end, "service" );
|
|
buf = tr_xmlTagContents( kk, end );
|
|
if( !tr_xmlFindTagVerifyContents( buf, end, "serviceType",
|
|
UPNP_SERVICE_TYPE, 1 ) )
|
|
{
|
|
*soap = tr_xmlDupTagContents( buf, end, "controlURL");
|
|
*scpd = tr_xmlDupTagContents( buf, end, "SCPDURL");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
basedup = tr_xmlDupContents( urlbase, end );
|
|
addUrlbase( basedup, soap );
|
|
addUrlbase( basedup, scpd );
|
|
free( basedup );
|
|
|
|
if( NULL != *soap && NULL != *scpd )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
addUrlbase( const char * base, char ** path )
|
|
{
|
|
const char * middle;
|
|
int len;
|
|
char * joined;
|
|
|
|
if( NULL == base || NULL == *path ||
|
|
'/' == **path || tr_httpIsUrl( *path, -1 ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
len = strlen( base );
|
|
middle = ( 0 >= len || '/' != base[len-1] ? "/" : "" );
|
|
joined = joinstrs( base, middle, *path );
|
|
free( *path );
|
|
*path = joined;
|
|
}
|
|
|
|
static int
|
|
parseScpd( const char * buf, int len, tr_upnp_action_t * getcmd,
|
|
tr_upnp_action_t * addcmd, tr_upnp_action_t * delcmd )
|
|
{
|
|
const char * end, * next, * sub, * name;
|
|
|
|
end = buf + len;
|
|
next = buf;
|
|
|
|
next = tr_xmlFindTagContents( next, end, "scpd" );
|
|
next = tr_xmlFindTagContents( next, end, "actionList" );
|
|
|
|
while( NULL != next )
|
|
{
|
|
next = tr_xmlFindTag( next, end, "action" );
|
|
sub = tr_xmlTagContents( next, end );
|
|
name = tr_xmlFindTagContents( sub, end, "name" );
|
|
sub = tr_xmlFindTagContents( sub, end, "argumentList" );
|
|
if( !tr_xmlVerifyContents( name, end, getcmd->name, 1 ) )
|
|
{
|
|
if( parseScpdArgs( sub, end, getcmd, 'i' ) ||
|
|
parseScpdArgs( sub, end, getcmd, 'o' ) )
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
else if( !tr_xmlVerifyContents( name, end, addcmd->name, 1 ) )
|
|
{
|
|
if( parseScpdArgs( sub, end, addcmd, 'i' ) )
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
else if( !tr_xmlVerifyContents( name, end, delcmd->name, 1 ) )
|
|
{
|
|
if( parseScpdArgs( sub, end, delcmd, 'i' ) )
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
next = tr_xmlSkipTag( next, end );
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parseScpdArgs( const char * buf, const char * end,
|
|
tr_upnp_action_t * action, char dir )
|
|
{
|
|
const char * sub, * which;
|
|
char * name, * var;
|
|
|
|
assert( 'i' == dir || 'o' == dir );
|
|
which = ( 'i' == dir ? "in" : "out" );
|
|
|
|
while( NULL != buf )
|
|
{
|
|
sub = tr_xmlFindTagContents( buf, end, "argument" );
|
|
if( !tr_xmlFindTagVerifyContents( sub, end, "direction", which, 1 ) )
|
|
{
|
|
name = tr_xmlDupTagContents( sub, end, "name" );
|
|
var = tr_xmlDupTagContents( sub, end, "relatedStateVariable" );
|
|
if( NULL == name || NULL == var )
|
|
{
|
|
free( name );
|
|
free( var );
|
|
}
|
|
else if( actionAdd( action, name, var, dir ) )
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
buf = tr_xmlSkipTag( buf, end );
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parseMapping( tr_upnp_device_t * dev, const char * buf, int len )
|
|
{
|
|
const char * end, * down, * next, * var;
|
|
int varlen, pret, cret, eret;
|
|
char * val;
|
|
|
|
assert( 0 < dev->mappedport );
|
|
|
|
if( NULL == dev->myaddr )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
pret = -1;
|
|
cret = -1;
|
|
eret = -1;
|
|
|
|
end = buf + len;
|
|
down = buf;
|
|
down = tr_xmlFindTagContents( down, end, "Envelope" );
|
|
down = tr_xmlFindTagContents( down, end, "Body" );
|
|
down = tr_xmlFindTagContents( down, end,
|
|
"GetSpecificPortMappingEntryResponse" );
|
|
|
|
next = down;
|
|
while( NULL != next )
|
|
{
|
|
var = tr_xmlTagName( next, end, &varlen );
|
|
var = actionLookupVar( &dev->getcmd, var, varlen, 'o' );
|
|
if( NULL != var )
|
|
{
|
|
val = tr_xmlDupContents( tr_xmlTagContents( next, end ), end );
|
|
if( 0 == tr_strcasecmp( "InternalPort", var ) )
|
|
{
|
|
pret = ( strtol( val, NULL, 10 ) == dev->mappedport ? 1 : 0 );
|
|
}
|
|
else if( 0 == tr_strcasecmp( "InternalClient", var ) )
|
|
{
|
|
cret = ( 0 == strcmp( dev->myaddr, val ) ? 1 : 0 );
|
|
}
|
|
else if( 0 == tr_strcasecmp( "PortMappingEnabled", var ) )
|
|
{
|
|
eret = ( 0 == strcmp( "1", val ) ? 1 : 0 );
|
|
}
|
|
free( val );
|
|
}
|
|
next = tr_xmlSkipTag( next, end );
|
|
}
|
|
|
|
return MIN( MIN( pret, cret), eret );
|
|
}
|
|
|
|
static char *
|
|
joinstrs( const char * first, const char * delim, const char * second )
|
|
{
|
|
char * ret;
|
|
int len1, len2, len3;
|
|
|
|
len1 = strlen( first );
|
|
len2 = strlen( delim );
|
|
len3 = strlen( second );
|
|
ret = malloc( len1 + len2 + len3 + 1 );
|
|
if( NULL == ret )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
memcpy( ret, first, len1 );
|
|
memcpy( ret + len1, delim, len2 );
|
|
memcpy( ret + len1 + len2, second, len3 );
|
|
ret[len1 + len2 + len3] = '\0';
|
|
|
|
return ret;
|
|
}
|
|
|
|
static tr_http_t *
|
|
soapRequest( int retry, const char * host, int port, const char * path,
|
|
tr_upnp_action_t * action, ... )
|
|
{
|
|
tr_http_t * http;
|
|
va_list ap;
|
|
const char * name, * value;
|
|
int method;
|
|
|
|
method = ( retry ? TR_HTTP_M_POST : TR_HTTP_POST );
|
|
http = makeHttp( method, host, port, path );
|
|
if( NULL != http )
|
|
{
|
|
tr_httpAddHeader( http, "Content-type",
|
|
"text/xml; encoding=\"utf-8\"" );
|
|
tr_httpAddHeader( http, "SOAPAction", action->action );
|
|
if( retry )
|
|
{
|
|
tr_httpAddHeader( http, "Man", "\"" SOAP_ENVELOPE "\"" );
|
|
}
|
|
tr_httpAddBody( http,
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
|
|
"<s:Envelope"
|
|
" xmlns:s=\"" SOAP_ENVELOPE "\""
|
|
" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
|
" <s:Body>"
|
|
" <u:%s xmlns:u=\"" UPNP_SERVICE_TYPE "\">", action->name );
|
|
|
|
va_start( ap, action );
|
|
do
|
|
{
|
|
name = va_arg( ap, const char * );
|
|
value = NULL;
|
|
name = actionLookupName( action, name, -1, 'i' );
|
|
if( NULL != name )
|
|
{
|
|
value = va_arg( ap, const char * );
|
|
if( NULL != value )
|
|
{
|
|
tr_httpAddBody( http,
|
|
" <%s>%s</%s>", name, value, name );
|
|
}
|
|
else
|
|
{
|
|
tr_httpAddBody( http,
|
|
" <%s></%s>", name, name );
|
|
}
|
|
}
|
|
}
|
|
while( NULL != name && NULL != value );
|
|
va_end( ap );
|
|
|
|
tr_httpAddBody( http,
|
|
" </u:%s>"
|
|
" </s:Body>"
|
|
"</s:Envelope>", action->name );
|
|
}
|
|
|
|
return http;
|
|
}
|
|
|
|
static void
|
|
actionSetup( tr_upnp_action_t * action, const char * name, int prealloc )
|
|
{
|
|
action->name = strdup( name );
|
|
action->action = NULL;
|
|
asprintf( &action->action, "\"%s#%s\"", UPNP_SERVICE_TYPE, name );
|
|
assert( NULL == action->args );
|
|
action->args = malloc( sizeof( *action->args ) * prealloc );
|
|
memset( action->args, 0, sizeof( *action->args ) * prealloc );
|
|
action->len = prealloc;
|
|
}
|
|
|
|
static void
|
|
actionFree( tr_upnp_action_t * act )
|
|
{
|
|
free( act->name );
|
|
free( act->action );
|
|
while( 0 < act->len )
|
|
{
|
|
act->len--;
|
|
free( act->args[act->len].name );
|
|
free( act->args[act->len].var );
|
|
}
|
|
free( act->args );
|
|
}
|
|
|
|
static int
|
|
actionAdd( tr_upnp_action_t * act, char * name, char * var, char dir )
|
|
{
|
|
int ii;
|
|
void * newbuf;
|
|
|
|
assert( 'i' == dir || 'o' == dir );
|
|
|
|
ii = 0;
|
|
while( ii < act->len && NULL != act->args[ii].name )
|
|
{
|
|
ii++;
|
|
}
|
|
|
|
if( ii == act->len )
|
|
{
|
|
newbuf = realloc( act->args, sizeof( *act->args ) * ( act->len + 1 ) );
|
|
if( NULL == newbuf )
|
|
{
|
|
return 1;
|
|
}
|
|
act->args = newbuf;
|
|
act->len++;
|
|
}
|
|
|
|
act->args[ii].name = name;
|
|
act->args[ii].var = var;
|
|
act->args[ii].dir = dir;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const char *
|
|
actionLookup( tr_upnp_action_t * act, const char * key, int len,
|
|
char dir, int getname )
|
|
{
|
|
int ii;
|
|
|
|
assert( 'i' == dir || 'o' == dir );
|
|
|
|
if( NULL == key || 0 == len )
|
|
{
|
|
return NULL;
|
|
}
|
|
if( 0 > len )
|
|
{
|
|
len = strlen( key );
|
|
}
|
|
|
|
for( ii = 0; ii < act->len; ii++ )
|
|
{
|
|
if( NULL != act->args[ii].name && dir == act->args[ii].dir )
|
|
{
|
|
if( !getname &&
|
|
0 == tr_strncasecmp( act->args[ii].name, key, len ) )
|
|
{
|
|
return act->args[ii].var;
|
|
}
|
|
else if( getname &&
|
|
0 == tr_strncasecmp( act->args[ii].var, key, len ) )
|
|
{
|
|
return act->args[ii].name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|