1044 lines
28 KiB
C
1044 lines
28 KiB
C
/* $Id: miniupnpc-async.c,v 1.19 2014/11/07 12:05:40 nanard Exp $ */
|
|
/* miniupnpc-async
|
|
* Copyright (c) 2008-2017, Thomas BERNARD <miniupnp@free.fr>
|
|
* http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <net/if.h>
|
|
#include <stdio.h>
|
|
#ifdef WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#include <io.h>
|
|
#define PRINT_SOCKET_ERROR printf
|
|
#define SOCKET_ERROR GetWSALastError()
|
|
#define WOULDBLOCK(err) (err == WSAEWOULDBLOCK)
|
|
#else
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#define closesocket close
|
|
#define PRINT_SOCKET_ERROR perror
|
|
#define SOCKET_ERROR errno
|
|
#define WOULDBLOCK(err) (err == EAGAIN || err == EWOULDBLOCK)
|
|
#endif
|
|
#include "miniupnpc-async.h"
|
|
#include "parsessdpreply.h"
|
|
#include "upnputils.h"
|
|
#include "minixml.h"
|
|
#include "igd_desc_parse.h"
|
|
#include "upnpreplyparse.h"
|
|
|
|
#ifndef MIN
|
|
#define MIN(x,y) (((x)<(y))?(x):(y))
|
|
#endif /* MIN */
|
|
|
|
#ifndef MAXHOSTNAMELEN
|
|
#define MAXHOSTNAMELEN 64
|
|
#endif /* MAXHOSTNAMELEN */
|
|
|
|
#define SSDP_PORT 1900
|
|
#define SSDP_MCAST_ADDR "239.255.255.250"
|
|
#define XSTR(s) STR(s)
|
|
#define STR(s) #s
|
|
|
|
#ifdef DEBUG
|
|
#define debug_printf(...) fprintf(stderr, __VA_ARGS__)
|
|
#else
|
|
#define debug_printf(...)
|
|
#endif
|
|
|
|
/* stuctures */
|
|
|
|
struct upnp_args {
|
|
const char * elt;
|
|
const char * val;
|
|
};
|
|
|
|
/* private functions */
|
|
|
|
static int upnpc_connect(upnpc_device_t * p, const char * url);
|
|
static int upnpc_send_request(upnpc_device_t * p);
|
|
|
|
|
|
/* parse_msearch_reply()
|
|
* the last 4 arguments are filled during the parsing :
|
|
* - location/locationsize : "location:" field of the SSDP reply packet
|
|
* - st/stsize : "st:" field of the SSDP reply packet.
|
|
* The strings are NOT null terminated */
|
|
static void
|
|
parse_msearch_reply(const char * reply, int size,
|
|
const char * * location, unsigned int * locationsize,
|
|
const char * * st, unsigned int * stsize)
|
|
{
|
|
int a, b, i;
|
|
i = 0; /* current character index */
|
|
a = i; /* start of the line */
|
|
b = 0; /* end of the "header" (position of the colon) */
|
|
while(i<size) {
|
|
switch(reply[i]) {
|
|
case ':':
|
|
if(b==0) {
|
|
b = i; /* end of the "header" */
|
|
}
|
|
break;
|
|
case '\x0a':
|
|
case '\x0d':
|
|
if(b!=0) {
|
|
/* skip the colon and white spaces */
|
|
do { b++; } while(reply[b]==' ' && b<size);
|
|
if(0==strncasecmp(reply+a, "location", 8)) {
|
|
*location = reply+b;
|
|
*locationsize = i-b;
|
|
} else if(0==strncasecmp(reply+a, "st", 2)) {
|
|
*st = reply+b;
|
|
*stsize = i-b;
|
|
}
|
|
b = 0;
|
|
}
|
|
a = i+1;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
|
|
static int upnpc_send_ssdp_msearch(upnpc_t * p, const char * device, unsigned int mx)
|
|
{
|
|
/* envoyer les packets de M-SEARCH discovery sur le socket ssdp */
|
|
int n;
|
|
char bufr[1024];
|
|
struct sockaddr_in addr;
|
|
static const char MSearchMsgFmt[] =
|
|
"M-SEARCH * HTTP/1.1\r\n"
|
|
"HOST: " SSDP_MCAST_ADDR ":" XSTR(SSDP_PORT) "\r\n"
|
|
"ST: %s\r\n"
|
|
"MAN: \"ssdp:discover\"\r\n"
|
|
"MX: %u\r\n"
|
|
"\r\n";
|
|
|
|
memset(&addr, 0, sizeof(struct sockaddr_in));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(SSDP_PORT);
|
|
addr.sin_addr.s_addr = inet_addr(SSDP_MCAST_ADDR);
|
|
n = snprintf(bufr, sizeof(bufr),
|
|
MSearchMsgFmt, device, mx);
|
|
debug_printf("upnpc_send_ssdp_msearch: %s", bufr);
|
|
n = sendto(p->ssdp_socket, bufr, n, 0,
|
|
(struct sockaddr *)&addr, sizeof(struct sockaddr_in));
|
|
if (n < 0) {
|
|
int err = SOCKET_ERROR;
|
|
if(err == EINTR || WOULDBLOCK(err)) {
|
|
debug_printf("upnpc_send_ssdp_msearch: should try again");
|
|
p->state = ESendSSDP;
|
|
return 0;
|
|
}
|
|
PRINT_SOCKET_ERROR("sendto");
|
|
return -1;
|
|
}
|
|
p->state = EReceiveSSDP;
|
|
return 0;
|
|
}
|
|
|
|
static int upnpc_set_root_desc_location(upnpc_device_t * d, const char * location, int locationsize)
|
|
{
|
|
char * tmp;
|
|
tmp = realloc(d->root_desc_location, locationsize + 1);
|
|
if(tmp == 0) {
|
|
return -1;
|
|
}
|
|
memcpy(tmp, location, locationsize);
|
|
tmp[locationsize] = '\0';
|
|
d->root_desc_location = tmp;
|
|
return 0;
|
|
}
|
|
|
|
static int upnpc_receive_and_parse_ssdp(upnpc_t * p)
|
|
{
|
|
int n;
|
|
char bufr[1024];
|
|
n = recv(p->ssdp_socket, bufr, sizeof(bufr), 0);
|
|
if (n<0) {
|
|
PRINT_SOCKET_ERROR("recv");
|
|
} else if (n==0) {
|
|
debug_printf("empty packet received\n");
|
|
} else {
|
|
const char * location = NULL;
|
|
unsigned int locationsize;
|
|
const char * st = NULL;
|
|
unsigned int stsize;
|
|
debug_printf("%.*s", n, bufr);
|
|
parse_msearch_reply(bufr, n, &location, &locationsize, &st, &stsize);
|
|
debug_printf("location = '%.*s'\n", locationsize, location);
|
|
debug_printf("st = '%.*s'\n", stsize, st);
|
|
if(location != NULL) {
|
|
upnpc_device_t * dev = p->device_list;
|
|
while(dev != NULL) {
|
|
if(dev->root_desc_location != NULL
|
|
&& strlen(dev->root_desc_location) == locationsize
|
|
&& memcmp(dev->root_desc_location, location, locationsize) == 0) {
|
|
debug_printf("device already in list (location='%s')\n", dev->root_desc_location);
|
|
return -1;
|
|
}
|
|
dev = dev->next;
|
|
}
|
|
dev = calloc(1, sizeof(upnpc_device_t));
|
|
if(dev == NULL) {
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
if(upnpc_set_root_desc_location(dev, location, locationsize) < 0) {
|
|
free(dev);
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
dev->next = p->device_list;
|
|
p->device_list = dev;
|
|
dev->state = EGetDescConnect;
|
|
upnpc_connect(dev, dev->root_desc_location);
|
|
} else {
|
|
/* or do nothing ? */
|
|
p->state = EError;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parseURL(const char * url,
|
|
char * hostname, unsigned short * port,
|
|
char * * path, unsigned int * scope_id)
|
|
{
|
|
char * p1, *p2, *p3;
|
|
if(!url)
|
|
return 0;
|
|
p1 = strstr(url, "://");
|
|
if(!p1)
|
|
return 0;
|
|
p1 += 3;
|
|
if( (url[0]!='h') || (url[1]!='t')
|
|
||(url[2]!='t') || (url[3]!='p'))
|
|
return 0;
|
|
memset(hostname, 0, MAXHOSTNAMELEN + 1);
|
|
if(*p1 == '[') {
|
|
/* IP v6 : http://[2a00:1450:8002::6a]/path/abc */
|
|
char * scope;
|
|
scope = strchr(p1, '%');
|
|
p2 = strchr(p1, ']');
|
|
if(p2 && scope && scope < p2 && scope_id) {
|
|
/* parse scope */
|
|
#ifdef IF_NAMESIZE
|
|
char tmp[IF_NAMESIZE];
|
|
int l;
|
|
scope++;
|
|
/* "%25" is just '%' in URL encoding */
|
|
if(scope[0] == '2' && scope[1] == '5')
|
|
scope += 2; /* skip "25" */
|
|
l = p2 - scope;
|
|
if(l >= IF_NAMESIZE)
|
|
l = IF_NAMESIZE - 1;
|
|
memcpy(tmp, scope, l);
|
|
tmp[l] = '\0';
|
|
*scope_id = if_nametoindex(tmp);
|
|
if(*scope_id == 0) {
|
|
*scope_id = (unsigned int)strtoul(tmp, NULL, 10);
|
|
}
|
|
#else
|
|
/* under windows, scope is numerical */
|
|
char tmp[8];
|
|
int l;
|
|
scope++;
|
|
/* "%25" is just '%' in URL encoding */
|
|
if(scope[0] == '2' && scope[1] == '5')
|
|
scope += 2; /* skip "25" */
|
|
l = p2 - scope;
|
|
if(l >= (int)sizeof(tmp))
|
|
l = sizeof(tmp) - 1;
|
|
memcpy(tmp, scope, l);
|
|
tmp[l] = '\0';
|
|
*scope_id = (unsigned int)strtoul(tmp, NULL, 10);
|
|
#endif
|
|
}
|
|
p3 = strchr(p1, '/');
|
|
if(p2 && p3) {
|
|
p2++;
|
|
strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1)));
|
|
if(*p2 == ':') {
|
|
*port = 0;
|
|
p2++;
|
|
while( (*p2 >= '0') && (*p2 <= '9')) {
|
|
*port *= 10;
|
|
*port += (unsigned short)(*p2 - '0');
|
|
p2++;
|
|
}
|
|
} else {
|
|
*port = 80;
|
|
}
|
|
*path = p3;
|
|
return 1;
|
|
}
|
|
}
|
|
p2 = strchr(p1, ':');
|
|
p3 = strchr(p1, '/');
|
|
if(!p3)
|
|
return 0;
|
|
if(!p2 || (p2>p3)) {
|
|
strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p3-p1)));
|
|
*port = 80;
|
|
} else {
|
|
strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1)));
|
|
*port = 0;
|
|
p2++;
|
|
while( (*p2 >= '0') && (*p2 <= '9')) {
|
|
*port *= 10;
|
|
*port += (unsigned short)(*p2 - '0');
|
|
p2++;
|
|
}
|
|
}
|
|
*path = p3;
|
|
return 1;
|
|
}
|
|
|
|
static int upnpc_connect(upnpc_device_t * p, const char * url)
|
|
{
|
|
int r;
|
|
char hostname[MAXHOSTNAMELEN+1];
|
|
unsigned short port;
|
|
char * path;
|
|
unsigned int scope_id;
|
|
struct sockaddr_in addr;
|
|
socklen_t addrlen;
|
|
|
|
/*if(p->root_desc_location == 0) {
|
|
p->state = EError;
|
|
return -1;
|
|
}*/
|
|
if(!parseURL(url/*p->root_desc_location*/, hostname, &port,
|
|
&path, &scope_id)) {
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
p->http_socket = socket(PF_INET, SOCK_STREAM, 0);
|
|
if(p->http_socket < 0) {
|
|
PRINT_SOCKET_ERROR("socket");
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
if(!set_non_blocking(p->http_socket)) {
|
|
/* TODO : ERROR */
|
|
}
|
|
memset(&addr, 0, sizeof(struct sockaddr_in));
|
|
addr.sin_family = AF_INET;
|
|
inet_pton(AF_INET, hostname, &(addr.sin_addr));
|
|
addr.sin_port = htons(port);
|
|
addrlen = sizeof(struct sockaddr_in);
|
|
do {
|
|
r = connect(p->http_socket, (struct sockaddr *)&addr, addrlen);
|
|
if(r < 0) {
|
|
if(errno == EINPROGRESS) {
|
|
/*p->state = EGetDescConnect;*/
|
|
return 0;
|
|
} else if(errno != EINTR) {
|
|
PRINT_SOCKET_ERROR("connect");
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
}
|
|
} while(r < 0 && errno == EINTR);
|
|
if(p->state == EGetDescConnect) {
|
|
p->state = EGetDescRequest;
|
|
} else {
|
|
p->state = ESoapRequest;
|
|
}
|
|
upnpc_send_request(p);
|
|
return 0;
|
|
}
|
|
|
|
static int upnpc_complete_connect(upnpc_device_t * p)
|
|
{
|
|
socklen_t len;
|
|
int err;
|
|
len = sizeof(err);
|
|
if(getsockopt(p->http_socket, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
|
|
PRINT_SOCKET_ERROR("getsockopt");
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
if(err != 0) {
|
|
debug_printf("connect failed %d\n", err);
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
if(p->state == EGetDescConnect)
|
|
p->state = EGetDescRequest;
|
|
else
|
|
p->state = ESoapRequest;
|
|
upnpc_send_request(p);
|
|
return 0;
|
|
}
|
|
|
|
static int upnpc_send_request(upnpc_device_t * p)
|
|
{
|
|
ssize_t n;
|
|
static const char reqfmt[] = "GET %s HTTP/1.1\r\n"
|
|
"Host: %s:%hu\r\n"
|
|
"Connection: Close\r\n"
|
|
"User-Agent: MiniUPnPc-async\r\n"
|
|
"\r\n";
|
|
|
|
/* retrieve "our" IP address used to connect to the UPnP device */
|
|
p->selfaddrlen = sizeof(struct sockaddr_storage);
|
|
if(getsockname(p->http_socket, (struct sockaddr *)&p->selfaddr, &p->selfaddrlen) < 0) {
|
|
PRINT_SOCKET_ERROR("getsockname()");
|
|
}
|
|
|
|
if(p->http_request == NULL) {
|
|
char hostname[MAXHOSTNAMELEN+1];
|
|
unsigned short port;
|
|
char * path;
|
|
unsigned int scope_id;
|
|
int len;
|
|
if(!parseURL(p->root_desc_location, hostname, &port,
|
|
&path, &scope_id)) {
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
len = snprintf(NULL, 0, reqfmt, path, hostname, port);
|
|
p->http_request = malloc(len + 1);
|
|
if(p->http_request == NULL) {
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
p->http_request_len = snprintf(p->http_request, len + 1,
|
|
reqfmt, path, hostname, port);
|
|
p->http_request_sent = 0;
|
|
}
|
|
n = send(p->http_socket, p->http_request + p->http_request_sent,
|
|
p->http_request_len - p->http_request_sent, 0/* flags */);
|
|
if(n < 0) {
|
|
PRINT_SOCKET_ERROR("send");
|
|
p->state = EError;
|
|
return -1;
|
|
} else {
|
|
debug_printf("sent %d bytes\n", (int)n);
|
|
/*if(n == 0) {
|
|
p->state = EError;
|
|
return -1;
|
|
}*/
|
|
p->http_request_sent += n;
|
|
if(p->http_request_sent >= p->http_request_len) {
|
|
/* all bytes sent */
|
|
#if 0
|
|
shutdown(p->http_socket, SHUT_WR); /* some routers don't like that */
|
|
#endif
|
|
free(p->http_request);
|
|
p->http_request = NULL;
|
|
p->http_request_len = 0;
|
|
if(p->state == EGetDescRequest)
|
|
p->state = EGetDescResponse;
|
|
else
|
|
p->state = ESoapResponse;
|
|
free(p->http_response);
|
|
p->http_response = NULL;
|
|
p->http_response_received = 0;
|
|
p->http_response_end_of_headers = 0;
|
|
/* get response */
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int upnpc_parse_headers(upnpc_device_t * p)
|
|
{
|
|
/* search for CR LF CR LF (end of headers)
|
|
* recognize also LF LF */
|
|
int i = 0;
|
|
while(i < (p->http_response_received-1) &&
|
|
p->http_response_end_of_headers == 0) {
|
|
if(p->http_response[i] == '\r') {
|
|
i++;
|
|
if(p->http_response[i] == '\n') {
|
|
i++;
|
|
if(i < p->http_response_received && p->http_response[i] == '\r') {
|
|
i++;
|
|
if(i < p->http_response_received && p->http_response[i] == '\n') {
|
|
p->http_response_end_of_headers = i + 1;
|
|
}
|
|
}
|
|
}
|
|
} else if(p->http_response[i] == '\n') {
|
|
i++;
|
|
if(p->http_response[i] == '\n') {
|
|
p->http_response_end_of_headers = i + 1;
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
if(p->http_response_end_of_headers != 0) {
|
|
int colon = 0;
|
|
int linestart = 0;
|
|
int valuestart = 0;
|
|
p->http_response_code = -1;
|
|
for(i = 0; i < p->http_response_end_of_headers - 1; i++) {
|
|
if(linestart == 0) {
|
|
/* reading HTTP response code on the 1st line */
|
|
if(p->http_response[i] == ' ' && p->http_response_code < 0)
|
|
p->http_response_code = 0;
|
|
else if(p->http_response[i] >= '0' && p->http_response[i] <= '9') {
|
|
p->http_response_code = p->http_response_code * 10 + (p->http_response[i] - '0');
|
|
} else if(p->http_response[i] == ' ')
|
|
linestart = 1;
|
|
}
|
|
if(colon <= linestart && p->http_response[i] == ':') {
|
|
colon = i;
|
|
while(i < p->http_response_end_of_headers - 1 &&
|
|
(p->http_response[i+1] == ' ' || p->http_response[i+1] == '\t'))
|
|
i++;
|
|
valuestart = i + 1;
|
|
} else if(p->http_response[i + 1] == '\r' ||
|
|
p->http_response[i + 1] == '\n') {
|
|
if(colon > linestart && valuestart > colon) {
|
|
debug_printf("header='%.*s', value='%.*s'\n",
|
|
colon-linestart, p->http_response+linestart,
|
|
i+1-valuestart, p->http_response+valuestart);
|
|
if(0==strncasecmp(p->http_response+linestart, "content-length", colon-linestart)) {
|
|
p->http_response_content_length = atoi(p->http_response + valuestart);
|
|
debug_printf("Content-Length: %d\n", p->http_response_content_length);
|
|
if(p->http_response_content_length < 0) {
|
|
debug_printf("Content-Length overflow ? setting to 0\n");
|
|
p->http_response_content_length = 0;
|
|
}
|
|
} else if(0==strncasecmp(p->http_response+linestart, "transfer-encoding", colon-linestart)
|
|
&& 0==strncasecmp(p->http_response+valuestart, "chunked", 7)) {
|
|
debug_printf("Chunked transfer-encoding !\n");
|
|
p->http_response_chunked = 1;
|
|
}
|
|
}
|
|
/* find next line */
|
|
while((i < p->http_response_received) &&
|
|
(p->http_response[i]=='\r' || p->http_response[i] == '\n'))
|
|
i++;
|
|
linestart = i;
|
|
colon = linestart;
|
|
valuestart = 0;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static char * build_url_string(const char * urlbase, const char * root_desc_url, const char * controlurl)
|
|
{
|
|
int l, n;
|
|
char * s;
|
|
const char * base;
|
|
char * p;
|
|
/* if controlurl is an absolute url, return it */
|
|
if(0 == memcmp("http://", controlurl, 7))
|
|
return strdup(controlurl);
|
|
base = (urlbase[0] == '\0') ? root_desc_url : urlbase;
|
|
n = strlen(base);
|
|
if(n > 7) {
|
|
p = strchr(base + 7, '/');
|
|
if(p)
|
|
n = p - base;
|
|
}
|
|
l = n + strlen(controlurl) + 1;
|
|
if(controlurl[0] != '/')
|
|
l++;
|
|
s = malloc(l);
|
|
if(s == NULL) return NULL;
|
|
memcpy(s, base, n);
|
|
if(controlurl[0] != '/')
|
|
s[n++] = '/';
|
|
memcpy(s + n, controlurl, l - n);
|
|
return s;
|
|
}
|
|
|
|
static int upnpc_get_response(upnpc_device_t * p)
|
|
{
|
|
ssize_t n;
|
|
ssize_t count;
|
|
char buffer[2048];
|
|
if(p->http_response_content_length > 0) {
|
|
count = p->http_response_content_length
|
|
+ p->http_response_end_of_headers
|
|
- p->http_response_received;
|
|
if(count > (ssize_t)sizeof(buffer)) count = sizeof(buffer);
|
|
} else {
|
|
count = sizeof(buffer);
|
|
}
|
|
debug_printf("recv(..., %d)\n", (int)count);
|
|
n = recv(p->http_socket, buffer, count, 0/* flags */);
|
|
if(n < 0) {
|
|
if(errno == EINTR || WOULDBLOCK(errno))
|
|
return 0; /* try again later */
|
|
PRINT_SOCKET_ERROR("read");
|
|
p->state = EError;
|
|
return -1;
|
|
} else if(n == 0) {
|
|
/* receiving finished */
|
|
debug_printf("%.*s\n", p->http_response_received, p->http_response);
|
|
close(p->http_socket);
|
|
p->http_socket = -1;
|
|
/* parse */
|
|
if(p->http_response_end_of_headers == 0) {
|
|
upnpc_parse_headers(p);
|
|
}
|
|
/* TODO : decode chunked transfer-encoding */
|
|
/* parse xml */
|
|
if(p->state == EGetDescResponse) {
|
|
struct IGDdatas igd;
|
|
struct xmlparser parser;
|
|
memset(&igd, 0, sizeof(struct IGDdatas));
|
|
memset(&parser, 0, sizeof(struct xmlparser));
|
|
parser.xmlstart = p->http_response + p->http_response_end_of_headers;
|
|
parser.xmlsize = p->http_response_received - p->http_response_end_of_headers;
|
|
parser.data = &igd;
|
|
parser.starteltfunc = IGDstartelt;
|
|
parser.endeltfunc = IGDendelt;
|
|
parser.datafunc = IGDdata;
|
|
parsexml(&parser);
|
|
#ifdef DEBUG
|
|
printIGD(&igd);
|
|
#endif /* DEBUG */
|
|
p->control_conn_url = build_url_string(igd.urlbase, p->root_desc_location, igd.first.controlurl);
|
|
p->control_cif_url = build_url_string(igd.urlbase, p->root_desc_location, igd.CIF.controlurl);
|
|
debug_printf("control_conn_url='%s'\n", p->control_conn_url);
|
|
debug_printf("control_cif_url='%s'\n", p->control_cif_url);
|
|
} else {
|
|
ClearNameValueList(&p->soap_response_data);
|
|
ParseNameValue(p->http_response + p->http_response_end_of_headers,
|
|
p->http_response_received - p->http_response_end_of_headers,
|
|
&p->soap_response_data);
|
|
}
|
|
free(p->http_response);
|
|
p->http_response = NULL;
|
|
p->http_response_received = 0;
|
|
p->http_response_end_of_headers = 0;
|
|
p->state = EReady;
|
|
} else {
|
|
/* receiving in progress */
|
|
debug_printf("received %d bytes:\n%.*s\n", (int)n, (int)n, buffer);
|
|
if(p->http_response == NULL) {
|
|
p->http_response = malloc(n);
|
|
if(p->http_response == NULL) {
|
|
debug_printf("failed to malloc %d bytes\n", (int)n);
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
p->http_response_received = n;
|
|
memcpy(p->http_response, buffer, n);
|
|
} else {
|
|
char * tmp = realloc(p->http_response, p->http_response_received + n);
|
|
if(tmp == NULL) {
|
|
debug_printf("failed to realloc %d bytes\n", (int)(p->http_response_received + n));
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
p->http_response = tmp;
|
|
memcpy(p->http_response + p->http_response_received, buffer, n);
|
|
p->http_response_received += n;
|
|
}
|
|
if(p->http_response_end_of_headers == 0) {
|
|
upnpc_parse_headers(p);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#define SOAPPREFIX "s"
|
|
#define SERVICEPREFIX "u"
|
|
#define SERVICEPREFIX2 'u'
|
|
|
|
static int upnpc_build_soap_request(upnpc_device_t * p, const char * url,
|
|
const char * service,
|
|
const char * action,
|
|
const struct upnp_args * args, int arg_count)
|
|
{
|
|
char * body;
|
|
const char fmt_soap[] =
|
|
"<?xml version=\"1.0\"?>\r\n"
|
|
"<" SOAPPREFIX ":Envelope "
|
|
"xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" "
|
|
SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
|
"<" SOAPPREFIX ":Body>"
|
|
"<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">"
|
|
"%s"
|
|
"</" SERVICEPREFIX ":%s>"
|
|
"</" SOAPPREFIX ":Body></" SOAPPREFIX ":Envelope>"
|
|
"\r\n";
|
|
int body_len;
|
|
const char fmt_http[] =
|
|
"POST %s HTTP/1.1\r\n"
|
|
"Host: %s%s\r\n"
|
|
"User-Agent: MiniUPnPc-async\r\n"
|
|
"Content-Length: %d\r\n"
|
|
"Content-Type: text/xml\r\n"
|
|
"SOAPAction: \"%s#%s\"\r\n"
|
|
"Connection: Close\r\n"
|
|
"Cache-Control: no-cache\r\n" /* ??? */
|
|
"Pragma: no-cache\r\n"
|
|
"\r\n"
|
|
"%s";
|
|
char hostname[MAXHOSTNAMELEN+1];
|
|
unsigned short port;
|
|
char * path;
|
|
unsigned int scope_id;
|
|
char portstr[8];
|
|
char * args_xml = NULL;
|
|
|
|
if(arg_count > 0) {
|
|
int i;
|
|
size_t l, n;
|
|
for(i = 0, l = 0; i < arg_count; i++) {
|
|
/* <ELT>VAL</ELT> */
|
|
l += strlen(args[i].elt) * 2 + strlen(args[i].val) + 5;
|
|
}
|
|
args_xml = malloc(++l);
|
|
if(args_xml == NULL) {
|
|
p->state = EError;
|
|
return -1;
|
|
}
|
|
for(i = 0, n = 0; i < arg_count && n < l; i++) {
|
|
/* <ELT>VAL</ELT> */
|
|
n += snprintf(args_xml + n, l - n, "<%s>%s</%s>",
|
|
args[i].elt, args[i].val, args[i].elt);
|
|
}
|
|
}
|
|
|
|
body_len = snprintf(NULL, 0, fmt_soap, action, service, args_xml?args_xml:"", action);
|
|
body = malloc(body_len + 1);
|
|
if(body == NULL) {
|
|
p->state = EError;
|
|
free(args_xml);
|
|
return -1;
|
|
}
|
|
if(snprintf(body, body_len + 1, fmt_soap, action, service, args_xml?args_xml:"", action) != body_len) {
|
|
debug_printf("snprintf() returned strange value...\n");
|
|
}
|
|
free(args_xml);
|
|
args_xml = NULL;
|
|
if(!parseURL(url, hostname, &port, &path, &scope_id)) {
|
|
p->state = EError;
|
|
free(body);
|
|
return -1;
|
|
}
|
|
if(port != 80)
|
|
snprintf(portstr, sizeof(portstr), ":%hu", port);
|
|
else
|
|
portstr[0] = '\0';
|
|
p->http_request_len = snprintf(NULL, 0, fmt_http,
|
|
path/*url*/, hostname, portstr, body_len, service, action, body);
|
|
free(p->http_request);
|
|
p->http_request = malloc(p->http_request_len + 1);
|
|
if(snprintf(p->http_request, p->http_request_len + 1, fmt_http,
|
|
path/*url*/, hostname, portstr, body_len, service, action, body) != p->http_request_len) {
|
|
debug_printf("snprintf() returned strange value...\n");
|
|
}
|
|
free(body);
|
|
debug_printf("%s", p->http_request);
|
|
p->http_request_sent = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* public functions */
|
|
int upnpc_init(upnpc_t * p, const char * multicastif)
|
|
{
|
|
int opt = 1;
|
|
struct sockaddr_in addr;
|
|
if(!p)
|
|
return UPNPC_ERR_INVALID_ARGS;
|
|
p->state = EError;
|
|
memset(p, 0, sizeof(upnpc_t)); /* clean everything */
|
|
/* open the socket for SSDP */
|
|
p->ssdp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
if(p->ssdp_socket < 0) {
|
|
return UPNPC_ERR_SOCKET_FAILED;
|
|
}
|
|
/* set REUSEADDR */
|
|
#ifdef WIN32
|
|
if(setsockopt(p->ssdp_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt)) < 0) {
|
|
#else
|
|
if(setsockopt(p->ssdp_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
|
|
#endif
|
|
/* non fatal error ! */
|
|
}
|
|
if(!set_non_blocking(p->ssdp_socket)) {
|
|
/* TODO log error */
|
|
}
|
|
|
|
/* receive address */
|
|
memset(&addr, 0, sizeof(struct sockaddr_in));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = INADDR_ANY;
|
|
/*addr.sin_port = htons(SSDP_PORT);*/
|
|
|
|
if(multicastif) {
|
|
struct in_addr mc_if;
|
|
mc_if.s_addr = inet_addr(multicastif);
|
|
addr.sin_addr.s_addr = mc_if.s_addr;
|
|
if(setsockopt(p->ssdp_socket, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&mc_if, sizeof(mc_if)) < 0) {
|
|
PRINT_SOCKET_ERROR("setsockopt");
|
|
/* non fatal error ! */
|
|
}
|
|
}
|
|
|
|
/* bind the socket to the ssdp address in order to receive responses */
|
|
if(bind(p->ssdp_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) != 0) {
|
|
close(p->ssdp_socket);
|
|
return UPNPC_ERR_BIND_FAILED;
|
|
}
|
|
|
|
p->state = EInit;
|
|
return UPNPC_OK;
|
|
}
|
|
|
|
int upnpc_finalize(upnpc_t * p)
|
|
{
|
|
if(!p) return UPNPC_ERR_INVALID_ARGS;
|
|
if(p->ssdp_socket >= 0) {
|
|
close(p->ssdp_socket);
|
|
p->ssdp_socket = -1;
|
|
}
|
|
while(p->device_list) {
|
|
upnpc_device_t * next = p->device_list->next;
|
|
free(p->device_list->root_desc_location);
|
|
p->device_list->root_desc_location = NULL;
|
|
free(p->device_list->http_request);
|
|
p->device_list->http_request = NULL;
|
|
free(p->device_list->http_response);
|
|
p->device_list->http_response = NULL;
|
|
free(p->device_list->control_cif_url);
|
|
p->device_list->control_cif_url = NULL;
|
|
free(p->device_list->control_conn_url);
|
|
p->device_list->control_conn_url = NULL;
|
|
if(p->device_list->http_socket >= 0) {
|
|
close(p->device_list->http_socket);
|
|
p->device_list->http_socket = -1;
|
|
}
|
|
ClearNameValueList(&p->device_list->soap_response_data);
|
|
free(p->device_list);
|
|
p->device_list = next;
|
|
}
|
|
p->state = EFinalized;
|
|
return UPNPC_OK;
|
|
}
|
|
|
|
int upnpc_get_external_ip_address(upnpc_device_t * p)
|
|
{
|
|
upnpc_build_soap_request(p, p->control_conn_url,
|
|
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
"GetExternalIPAddress", NULL, 0);
|
|
p->state = ESoapConnect;
|
|
upnpc_connect(p, p->control_conn_url);
|
|
return 0;
|
|
}
|
|
|
|
int upnpc_get_link_layer_max_rate(upnpc_device_t * p)
|
|
{
|
|
upnpc_build_soap_request(p, p->control_cif_url,
|
|
"urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
|
|
"GetCommonLinkProperties", NULL, 0);
|
|
p->state = ESoapConnect;
|
|
upnpc_connect(p, p->control_conn_url);
|
|
return 0;
|
|
}
|
|
|
|
int upnpc_add_port_mapping(upnpc_device_t * p,
|
|
const char * remote_host, unsigned short ext_port,
|
|
unsigned short int_port, const char * int_client,
|
|
const char * proto, const char * description,
|
|
unsigned int lease_duration)
|
|
{
|
|
struct upnp_args args[8];
|
|
char lease_duration_str[16];
|
|
char int_port_str[8];
|
|
char ext_port_str[8];
|
|
|
|
if(int_client == NULL || int_port == 0 || ext_port == 0 || proto == NULL)
|
|
return UPNPC_ERR_INVALID_ARGS;
|
|
snprintf(lease_duration_str, sizeof(lease_duration_str), "%u", lease_duration);
|
|
snprintf(int_port_str, sizeof(int_port_str), "%hu", int_port);
|
|
snprintf(ext_port_str, sizeof(ext_port_str), "%hu", ext_port);
|
|
args[0].elt = "NewRemoteHost";
|
|
args[0].val = remote_host?remote_host:"";
|
|
args[1].elt = "NewExternalPort";
|
|
args[1].val = ext_port_str;
|
|
args[2].elt = "NewProtocol";
|
|
args[2].val = proto;
|
|
args[3].elt = "NewInternalPort";
|
|
args[3].val = int_port_str;
|
|
args[4].elt = "NewInternalClient";
|
|
args[4].val = int_client;
|
|
args[5].elt = "NewEnabled";
|
|
args[5].val = "1";
|
|
args[6].elt = "NewPortMappingDescription";
|
|
args[6].val = description?description:"miniupnpc-async";
|
|
args[7].elt = "NewLeaseDuration";
|
|
args[7].val = lease_duration_str;
|
|
upnpc_build_soap_request(p, p->control_conn_url,
|
|
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
"AddPortMapping",
|
|
args, 8);
|
|
p->state = ESoapConnect;
|
|
upnpc_connect(p, p->control_conn_url);
|
|
return 0;
|
|
}
|
|
|
|
#ifdef UPNPC_USE_SELECT
|
|
int upnpc_select_fds(upnpc_t * p, int * nfds, fd_set * readfds, fd_set * writefds)
|
|
{
|
|
upnpc_device_t * d;
|
|
int n = 0;
|
|
if(!p) return UPNPC_ERR_INVALID_ARGS;
|
|
for(d = p->device_list; d != NULL; d = d->next) {
|
|
switch(d->state) {
|
|
case EGetDescConnect:
|
|
case EGetDescRequest:
|
|
case ESoapConnect:
|
|
case ESoapRequest:
|
|
FD_SET(d->http_socket, writefds);
|
|
if(*nfds < d->http_socket)
|
|
*nfds = d->http_socket;
|
|
n++;
|
|
break;
|
|
case EGetDescResponse:
|
|
case ESoapResponse:
|
|
FD_SET(d->http_socket, readfds);
|
|
if(*nfds < d->http_socket)
|
|
*nfds = d->http_socket;
|
|
n++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch(p->state) {
|
|
case ESendSSDP:
|
|
FD_SET(p->ssdp_socket, writefds);
|
|
if(*nfds < p->ssdp_socket)
|
|
*nfds = p->ssdp_socket;
|
|
n++;
|
|
break;
|
|
case EReceiveSSDP:
|
|
default:
|
|
/* still receive SSDP responses when processing Description, etc. */
|
|
FD_SET(p->ssdp_socket, readfds);
|
|
if(*nfds < p->ssdp_socket)
|
|
*nfds = p->ssdp_socket;
|
|
n++;
|
|
break;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
void upnpc_check_select_fds(upnpc_t * p, const fd_set * readfds, const fd_set * writefds)
|
|
{
|
|
upnpc_device_t * d;
|
|
|
|
p->socket_flags = 0;
|
|
if(FD_ISSET(p->ssdp_socket, readfds))
|
|
p->socket_flags = UPNPC_SSDP_READABLE;
|
|
if(FD_ISSET(p->ssdp_socket, writefds))
|
|
p->socket_flags = UPNPC_SSDP_WRITEABLE;
|
|
|
|
for(d = p->device_list; d != NULL; d = d->next) {
|
|
d->socket_flags = 0;
|
|
if(FD_ISSET(d->http_socket, readfds))
|
|
d->socket_flags = UPNPC_HTTP_READABLE;
|
|
if(FD_ISSET(d->http_socket, writefds))
|
|
d->socket_flags = UPNPC_HTTP_WRITEABLE;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static const char * devices_to_search[] = {
|
|
"urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
"urn:schemas-upnp-org:service:WANIPConnection:1",
|
|
"urn:schemas-upnp-org:service:WANPPPConnection:1",
|
|
"upnp:rootdevice",
|
|
0
|
|
};
|
|
|
|
int upnpc_process(upnpc_t * p)
|
|
{
|
|
upnpc_device_t * d;
|
|
/*
|
|
1) Envoyer les paquets de discovery SSDP
|
|
2) Recevoir et traiter les reponses
|
|
3) recup les descriptions
|
|
4) tester les etats
|
|
*/
|
|
if(!p) return UPNPC_ERR_INVALID_ARGS;
|
|
debug_printf("state=%d socket_flags=0x%04x\n", (int)p->state, p->socket_flags);
|
|
|
|
for(d = p->device_list; d != NULL; d = d->next) {
|
|
switch(d->state) {
|
|
case EGetDescConnect:
|
|
case ESoapConnect:
|
|
upnpc_complete_connect(d);
|
|
break;
|
|
case EGetDescRequest:
|
|
case ESoapRequest:
|
|
upnpc_send_request(d);
|
|
break;
|
|
case EGetDescResponse:
|
|
case ESoapResponse:
|
|
upnpc_get_response(d);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
/* all devices ready => ready */
|
|
if(p->device_list != NULL) {
|
|
d = p->device_list;
|
|
while(d && d->state == EReady) d = d->next;
|
|
p->state = (d == NULL) ? EReady : EProcessing;
|
|
}
|
|
|
|
if(p->socket_flags & UPNPC_SSDP_READABLE) {
|
|
upnpc_receive_and_parse_ssdp(p);
|
|
}
|
|
switch(p->state) {
|
|
case EInit:
|
|
upnpc_send_ssdp_msearch(p, devices_to_search[0], 2);
|
|
break;
|
|
case ESendSSDP:
|
|
upnpc_send_ssdp_msearch(p, devices_to_search[0], 2);
|
|
break;
|
|
case EReceiveSSDP:
|
|
/*upnpc_receive_and_parse_ssdp(p);*/
|
|
break;
|
|
/*case EGetDesc:
|
|
upnpc_connect(p);
|
|
break;*/
|
|
case EReady:
|
|
case EProcessing:
|
|
break;
|
|
default:
|
|
return UPNPC_ERR_UNKNOWN_STATE;
|
|
}
|
|
return UPNPC_OK;
|
|
}
|
|
|