917 lines
28 KiB
C
917 lines
28 KiB
C
/*
|
|
* Copyright (C) 2022 Teluu Inc. (http://www.teluu.com)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
#include <pjnath/upnp.h>
|
|
#include <pjnath/config.h>
|
|
#include <pj/addr_resolv.h>
|
|
#include <pj/assert.h>
|
|
#include <pj/errno.h>
|
|
#include <pj/log.h>
|
|
#include <pj/os.h>
|
|
#include <pj/pool.h>
|
|
#include <pj/string.h>
|
|
|
|
#if defined(PJNATH_HAS_UPNP) && (PJNATH_HAS_UPNP != 0)
|
|
|
|
#include <upnp/upnp.h>
|
|
#include <upnp/upnpdebug.h>
|
|
#include <upnp/upnptools.h>
|
|
|
|
#define THIS_FILE "upnp.c"
|
|
|
|
#define TRACE_(...) // PJ_LOG(6, (THIS_FILE, ##__VA_ARGS__))
|
|
|
|
/* Set to 1 to enable UPnP native logging */
|
|
#define ENABLE_LOG 0
|
|
|
|
/* Maximum number of devices. */
|
|
#define MAX_DEVS 16
|
|
|
|
|
|
/* UPnP device descriptions. */
|
|
static const char* UPNP_ROOT_DEVICE = "upnp:rootdevice";
|
|
static const char* UPNP_IGD_DEVICE =
|
|
"urn:schemas-upnp-org:device:InternetGatewayDevice:1";
|
|
static const char* UPNP_WANIP_SERVICE =
|
|
"urn:schemas-upnp-org:service:WANIPConnection:1";
|
|
static const char* UPNP_WANPPP_SERVICE =
|
|
"urn:schemas-upnp-org:service:WANPPPConnection:1";
|
|
|
|
|
|
/* Structure for IGD device. */
|
|
struct igd
|
|
{
|
|
pj_str_t dev_id;
|
|
pj_str_t url;
|
|
pj_str_t service_type;
|
|
pj_str_t control_url;
|
|
pj_str_t public_ip;
|
|
pj_sockaddr public_ip_addr;
|
|
|
|
pj_bool_t valid;
|
|
pj_bool_t alive;
|
|
};
|
|
|
|
/* UPnP manager. */
|
|
static struct upnp
|
|
{
|
|
unsigned initialized;
|
|
pj_pool_t *pool;
|
|
pj_thread_desc thread_desc;
|
|
pj_thread_t *thread;
|
|
pj_mutex_t *mutex;
|
|
int search_cnt;
|
|
pj_status_t status;
|
|
|
|
unsigned igd_cnt;
|
|
struct igd igd_devs[20];
|
|
int primary_igd_idx;
|
|
|
|
UpnpClient_Handle client_hnd;
|
|
void (*upnp_cb)(pj_status_t status);
|
|
} upnp_mgr;
|
|
|
|
|
|
/* Get the value of the node. */
|
|
static const char * get_node_value(IXML_Node *node)
|
|
{
|
|
const char *ret = NULL;
|
|
if (node) {
|
|
IXML_Node* child = ixmlNode_getFirstChild(node);
|
|
if (child)
|
|
ret = ixmlNode_getNodeValue(child);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* Get the value of the first element in the doc with the specified name. */
|
|
static const char * doc_get_elmt_value(IXML_Document *doc, const char *name)
|
|
{
|
|
const char *ret = NULL;
|
|
IXML_NodeList *node_list = ixmlDocument_getElementsByTagName(doc, name);
|
|
if (node_list) {
|
|
ret = get_node_value(ixmlNodeList_item(node_list, 0));
|
|
ixmlNodeList_free(node_list);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* Get the value of the first element with the specified name. */
|
|
static const char * elmt_get_elmt_value(IXML_Element *elmt, const char *name)
|
|
{
|
|
const char *ret = NULL;
|
|
IXML_NodeList *node_list = ixmlElement_getElementsByTagName(elmt, name);
|
|
if (node_list) {
|
|
ret = get_node_value(ixmlNodeList_item(node_list, 0));
|
|
ixmlNodeList_free(node_list);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* Check if response contains errorCode. */
|
|
static const char * check_error_response(IXML_Document *doc)
|
|
{
|
|
const char *error_code = doc_get_elmt_value(doc, "errorCode");
|
|
|
|
if (error_code) {
|
|
const char *error_desc = doc_get_elmt_value(doc, "errorDescription");
|
|
|
|
PJ_LOG(3, (THIS_FILE, "Response error code: %s (%s)",
|
|
error_code, error_desc));
|
|
}
|
|
|
|
return error_code;
|
|
}
|
|
|
|
/* Query the external IP of the IGD. */
|
|
static const char *action_get_external_ip(struct igd *igd)
|
|
{
|
|
static const char* action_name = "GetExternalIPAddress";
|
|
IXML_Document *action = NULL;
|
|
IXML_Document *response = NULL;
|
|
const char *public_ip = NULL;
|
|
int upnp_err;
|
|
|
|
/* Create action XML. */
|
|
action = UpnpMakeAction(action_name, igd->service_type.ptr, 0, NULL);
|
|
if (!action) {
|
|
PJ_LOG(3, (THIS_FILE, "Failed to make GetExternalIPAddress action"));
|
|
return NULL;
|
|
}
|
|
|
|
/* Send the action XML. */
|
|
upnp_err = UpnpSendAction(upnp_mgr.client_hnd, igd->control_url.ptr,
|
|
igd->service_type.ptr, NULL, action, &response);
|
|
if (upnp_err != UPNP_E_SUCCESS || !response) {
|
|
PJ_LOG(3, (THIS_FILE, "Failed to send GetExternalIPAddress action: %s",
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
goto on_error;
|
|
}
|
|
|
|
if (check_error_response(response))
|
|
goto on_error;
|
|
|
|
/* Get the external IP address from the response. */
|
|
public_ip = doc_get_elmt_value(response, "NewExternalIPAddress");
|
|
if (!public_ip) {
|
|
PJ_LOG(3, (THIS_FILE, "IGD %s has no external IP", igd->dev_id.ptr));
|
|
goto on_error;
|
|
}
|
|
pj_strdup2_with_null(upnp_mgr.pool, &igd->public_ip, public_ip);
|
|
pj_sockaddr_parse(pj_AF_UNSPEC(), 0, &igd->public_ip,
|
|
&igd->public_ip_addr);
|
|
public_ip = igd->public_ip.ptr;
|
|
|
|
on_error:
|
|
ixmlDocument_free(action);
|
|
if (response) ixmlDocument_free(response);
|
|
|
|
return public_ip;
|
|
}
|
|
|
|
/* Download the XML document of the IGD. */
|
|
static void download_igd_xml(unsigned dev_idx)
|
|
{
|
|
struct igd *igd_dev = &upnp_mgr.igd_devs[dev_idx];
|
|
const char *url = igd_dev->url.ptr;
|
|
IXML_Document *doc = NULL;
|
|
int upnp_err;
|
|
const char *dev_type;
|
|
const char *friendly_name;
|
|
const char *base_url;
|
|
const char *control_url;
|
|
const char *public_ip;
|
|
char *abs_control_url = NULL;
|
|
IXML_NodeList *service_list = NULL;
|
|
unsigned i, n;
|
|
|
|
upnp_err = UpnpDownloadXmlDoc(url, &doc);
|
|
if (upnp_err != UPNP_E_SUCCESS || !doc) {
|
|
PJ_LOG(3, (THIS_FILE, "Error downloading device XML doc from %s: %s",
|
|
url, UpnpGetErrorMessage(upnp_err)));
|
|
goto on_error;
|
|
}
|
|
|
|
/* Check device type. */
|
|
dev_type = doc_get_elmt_value(doc, "deviceType");
|
|
if (!dev_type) return;
|
|
if (pj_ansi_strcmp(dev_type, UPNP_IGD_DEVICE) != 0) {
|
|
/* Device type is not IGD. */
|
|
goto on_error;
|
|
}
|
|
|
|
/* Get friendly name. */
|
|
friendly_name = doc_get_elmt_value(doc, "friendlyName");
|
|
if (!friendly_name)
|
|
friendly_name = "";
|
|
|
|
/* Get base URL. */
|
|
base_url = doc_get_elmt_value(doc, "URLBase");
|
|
if (!base_url)
|
|
base_url = url;
|
|
|
|
/* Get list of services defined by serviceType. */
|
|
service_list = ixmlDocument_getElementsByTagName(doc, "serviceType");
|
|
n = ixmlNodeList_length(service_list);
|
|
|
|
for (i = 0; i < n; i++) {
|
|
IXML_Node *service_type_node = ixmlNodeList_item(service_list, i);
|
|
IXML_Node *service_node = ixmlNode_getParentNode(service_type_node);
|
|
IXML_Element* service_element = (IXML_Element*) service_node;
|
|
const char *service_type;
|
|
pj_bool_t call_cb = PJ_FALSE;
|
|
|
|
/* Check if parent node is "service". */
|
|
if (!service_node ||
|
|
(pj_ansi_strcmp(ixmlNode_getNodeName(service_node), "service")))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/* We only want serviceType of WANIPConnection or WANPPPConnection. */
|
|
service_type = get_node_value(service_type_node);
|
|
if (pj_ansi_strcmp(service_type, UPNP_WANIP_SERVICE) &&
|
|
pj_ansi_strcmp(service_type, UPNP_WANPPP_SERVICE))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/* Get the controlURL. */
|
|
control_url = elmt_get_elmt_value(service_element, "controlURL");
|
|
if (!control_url)
|
|
continue;
|
|
|
|
/* Resolve the absolute address of controlURL. */
|
|
upnp_err = UpnpResolveURL2(base_url, control_url, &abs_control_url);
|
|
if (upnp_err == UPNP_E_SUCCESS) {
|
|
pj_strdup2_with_null(upnp_mgr.pool, &igd_dev->control_url,
|
|
abs_control_url);
|
|
free(abs_control_url);
|
|
} else {
|
|
PJ_LOG(4, (THIS_FILE, "Error resolving absolute controlURL: %s",
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
pj_strdup2_with_null(upnp_mgr.pool, &igd_dev->control_url,
|
|
control_url);
|
|
}
|
|
|
|
pj_strdup2_with_null(upnp_mgr.pool, &igd_dev->service_type, service_type);
|
|
|
|
/* Get the public IP of the IGD. */
|
|
public_ip = action_get_external_ip(igd_dev);
|
|
if (!public_ip)
|
|
break;
|
|
|
|
/* We find a valid IGD. */
|
|
igd_dev->valid = PJ_TRUE;
|
|
igd_dev->alive = PJ_TRUE;
|
|
|
|
PJ_LOG(4, (THIS_FILE, "Valid IGD:\n"
|
|
"\tUDN : %s\n"
|
|
"\tName : %s\n"
|
|
"\tService Type : %s\n"
|
|
"\tControl URL : %s\n"
|
|
"\tPublic IP : %s",
|
|
igd_dev->dev_id.ptr,
|
|
friendly_name,
|
|
igd_dev->service_type.ptr,
|
|
igd_dev->control_url.ptr,
|
|
public_ip));
|
|
|
|
/* Use this as primary IGD if we haven't had one. */
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
if (upnp_mgr.primary_igd_idx < 0) {
|
|
upnp_mgr.primary_igd_idx = dev_idx;
|
|
call_cb = PJ_TRUE;
|
|
upnp_mgr.status = PJ_SUCCESS;
|
|
}
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
|
|
if (call_cb && upnp_mgr.upnp_cb) {
|
|
(*upnp_mgr.upnp_cb)(upnp_mgr.status);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
on_error:
|
|
if (service_list)
|
|
ixmlNodeList_free(service_list);
|
|
if (doc)
|
|
ixmlDocument_free(doc);
|
|
}
|
|
|
|
/* Add a newly discovered IGD. */
|
|
static void add_device(const char *dev_id, const char *url)
|
|
{
|
|
unsigned i;
|
|
|
|
if (upnp_mgr.igd_cnt >= MAX_DEVS) {
|
|
PJ_LOG(3, (THIS_FILE, "Warning: Too many UPnP devices discovered"));
|
|
return;
|
|
}
|
|
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
for (i = 0; i < upnp_mgr.igd_cnt; i++) {
|
|
if (!pj_strcmp2(&upnp_mgr.igd_devs[i].dev_id, dev_id) &&
|
|
!pj_strcmp2(&upnp_mgr.igd_devs[i].url, url))
|
|
{
|
|
/* Device exists. */
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
pj_strdup2_with_null(upnp_mgr.pool,
|
|
&upnp_mgr.igd_devs[upnp_mgr.igd_cnt].dev_id, dev_id);
|
|
pj_strdup2_with_null(upnp_mgr.pool,
|
|
&upnp_mgr.igd_devs[upnp_mgr.igd_cnt++].url, url);
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
|
|
PJ_LOG(4, (THIS_FILE, "Discovered a new IGD %s, url: %s", dev_id, url));
|
|
|
|
/* Download the IGD's XML doc. */
|
|
download_igd_xml(upnp_mgr.igd_cnt-1);
|
|
}
|
|
|
|
/* Update online status of an IGD. */
|
|
static void set_device_online(const char *dev_id)
|
|
{
|
|
unsigned i;
|
|
|
|
for (i = 0; i < upnp_mgr.igd_cnt; i++) {
|
|
struct igd *igd = &upnp_mgr.igd_devs[i];
|
|
|
|
/* We are only interested in valid IGDs that we can use. */
|
|
if (!pj_strcmp2(&igd->dev_id, dev_id) && igd->valid) {
|
|
igd->alive = PJ_TRUE;
|
|
|
|
if (upnp_mgr.primary_igd_idx < 0) {
|
|
/* If we don't have a primary IGD, use this. */
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
upnp_mgr.primary_igd_idx = i;
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
|
|
PJ_LOG(4, (THIS_FILE, "Using primary IGD %s",
|
|
upnp_mgr.igd_devs[i].dev_id.ptr));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Update IGD status to offline. */
|
|
static void set_device_offline(const char *dev_id)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < (int)upnp_mgr.igd_cnt; i++) {
|
|
struct igd *igd = &upnp_mgr.igd_devs[i];
|
|
|
|
/* We are only interested in valid IGDs that we can use. */
|
|
if (!pj_strcmp2(&igd->dev_id, dev_id) && igd->valid) {
|
|
igd->alive = PJ_FALSE;
|
|
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
if (i == upnp_mgr.primary_igd_idx) {
|
|
unsigned j;
|
|
|
|
/* The primary IGD is offline, try to find another one. */
|
|
upnp_mgr.primary_igd_idx = -1;
|
|
for (j = 0; j < upnp_mgr.igd_cnt; j++) {
|
|
igd = &upnp_mgr.igd_devs[j];
|
|
if (igd->valid && igd->alive) {
|
|
upnp_mgr.primary_igd_idx = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
PJ_LOG(4, (THIS_FILE, "Device %s offline, now using IGD %s",
|
|
upnp_mgr.igd_devs[i].dev_id.ptr,
|
|
(upnp_mgr.primary_igd_idx < 0? "(none)":
|
|
igd->dev_id.ptr)));
|
|
}
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* UPnP client callback. */
|
|
static int client_cb(Upnp_EventType event_type, const void *event,
|
|
void * user_data)
|
|
{
|
|
/* Ignore if already uninitialized or incorrect user data. */
|
|
if (!upnp_mgr.initialized || user_data != &upnp_mgr)
|
|
return UPNP_E_SUCCESS;
|
|
|
|
if (!pj_thread_is_registered()) {
|
|
pj_bzero(upnp_mgr.thread_desc, sizeof(pj_thread_desc));
|
|
pj_thread_register("upnp_cb", upnp_mgr.thread_desc,
|
|
&upnp_mgr.thread);
|
|
}
|
|
|
|
switch (event_type) {
|
|
case UPNP_DISCOVERY_SEARCH_RESULT:
|
|
{
|
|
const UpnpDiscovery *d_event = (const UpnpDiscovery *) event;
|
|
int upnp_status = UpnpDiscovery_get_ErrCode(d_event);
|
|
const char *dev_id, *location;
|
|
|
|
if (upnp_status != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "UPnP discovery error: %s",
|
|
UpnpGetErrorMessage(upnp_status)));
|
|
break;
|
|
}
|
|
|
|
dev_id = UpnpDiscovery_get_DeviceID_cstr(d_event);
|
|
location = UpnpDiscovery_get_Location_cstr(d_event);
|
|
|
|
add_device(dev_id, location);
|
|
break;
|
|
}
|
|
case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
|
|
{
|
|
const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
|
|
set_device_online(UpnpDiscovery_get_DeviceID_cstr(d_event));
|
|
break;
|
|
}
|
|
|
|
case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
|
|
{
|
|
const UpnpDiscovery* d_event = (const UpnpDiscovery*) event;
|
|
set_device_offline(UpnpDiscovery_get_DeviceID_cstr(d_event));
|
|
break;
|
|
}
|
|
|
|
case UPNP_DISCOVERY_SEARCH_TIMEOUT:
|
|
{
|
|
pj_bool_t call_cb = PJ_FALSE;
|
|
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
if (upnp_mgr.search_cnt > 0) {
|
|
--upnp_mgr.search_cnt;
|
|
if (upnp_mgr.search_cnt == 0 && upnp_mgr.primary_igd_idx < 0) {
|
|
PJ_LOG(4,(THIS_FILE, "Search timed out, no valid IGD found"));
|
|
call_cb = PJ_TRUE;
|
|
upnp_mgr.status = PJ_ENOTFOUND;
|
|
}
|
|
}
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
|
|
if (call_cb && upnp_mgr.upnp_cb) {
|
|
(*upnp_mgr.upnp_cb)(upnp_mgr.status);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case UPNP_CONTROL_ACTION_COMPLETE:
|
|
{
|
|
int err_code;
|
|
IXML_Document *response = NULL;
|
|
const UpnpActionComplete* a_event = (const UpnpActionComplete *) event;
|
|
if (!a_event)
|
|
break;
|
|
|
|
/* The only action complete event we're supposed to receive is
|
|
* from port mapping deletion action.
|
|
*/
|
|
err_code = UpnpActionComplete_get_ErrCode(a_event);
|
|
if (err_code != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Port mapping deletion action complete "
|
|
"error: %d (%s)", err_code,
|
|
UpnpGetErrorMessage(err_code)));
|
|
break;
|
|
}
|
|
|
|
response = UpnpActionComplete_get_ActionResult(a_event);
|
|
if (!response) {
|
|
PJ_LOG(4, (THIS_FILE, "Failed to get response to delete port "
|
|
"mapping"));
|
|
} else {
|
|
if (!check_error_response(response)) {
|
|
PJ_LOG(4, (THIS_FILE, "Successfully deleted port mapping"));
|
|
}
|
|
ixmlDocument_free(response);
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
TRACE_("Unhandled UPnP client callback %d", event_type);
|
|
break;
|
|
}
|
|
|
|
return UPNP_E_SUCCESS;
|
|
}
|
|
|
|
/* Initiate search for Internet Gateway Devices. */
|
|
static void search_igd(int search_time)
|
|
{
|
|
int err;
|
|
|
|
upnp_mgr.search_cnt = 4;
|
|
|
|
err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time,
|
|
UPNP_ROOT_DEVICE, &upnp_mgr);
|
|
if (err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Searching for UPNP_ROOT_DEVICE failed: %s",
|
|
UpnpGetErrorMessage(err)));
|
|
}
|
|
|
|
err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time,
|
|
UPNP_IGD_DEVICE, &upnp_mgr);
|
|
if (err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Searching for UPNP_IGD_DEVICE failed: %s",
|
|
UpnpGetErrorMessage(err)));
|
|
}
|
|
|
|
err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time,
|
|
UPNP_WANIP_SERVICE, &upnp_mgr);
|
|
if (err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Searching for UPNP_WANIP_SERVICE failed: %s",
|
|
UpnpGetErrorMessage(err)));
|
|
}
|
|
|
|
err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time,
|
|
UPNP_WANPPP_SERVICE, &upnp_mgr);
|
|
if (err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Searching for UPNP_WANPPP_SERVICE failed: %s",
|
|
UpnpGetErrorMessage(err)));
|
|
}
|
|
}
|
|
|
|
/* Initialize UPnP. */
|
|
PJ_DEF(pj_status_t) pj_upnp_init(const pj_upnp_init_param *param)
|
|
{
|
|
int upnp_err;
|
|
const char *ip_address;
|
|
unsigned short port;
|
|
const char *ip_address6 = NULL;
|
|
unsigned short port6 = 0;
|
|
|
|
if (upnp_mgr.initialized)
|
|
return PJ_SUCCESS;
|
|
|
|
#if ENABLE_LOG
|
|
UpnpSetLogLevel(UPNP_ALL);
|
|
UpnpSetLogFileNames("upnp.log", NULL);
|
|
upnp_err = UpnpInitLog();
|
|
if (upnp_err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Failed to initialize UPnP log: %s",
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
}
|
|
#endif
|
|
|
|
pj_bzero(&upnp_mgr, sizeof(upnp_mgr));
|
|
upnp_err = UpnpInit2(param->if_name, (unsigned short)param->port);
|
|
if (upnp_err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(1, (THIS_FILE, "Failed to initialize libupnp with "
|
|
"interface %s: %s",
|
|
(param->if_name? param->if_name: "NULL"),
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
return PJ_EUNKNOWN;
|
|
}
|
|
|
|
/* Register client. */
|
|
upnp_err = UpnpRegisterClient(client_cb, &upnp_mgr, &upnp_mgr.client_hnd);
|
|
if (upnp_err != UPNP_E_SUCCESS) {
|
|
PJ_LOG(1, (THIS_FILE, "Failed to register client: %s",
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
UpnpFinish();
|
|
return PJ_EINVALIDOP;
|
|
}
|
|
|
|
/* Try to disable web server. */
|
|
if (UpnpIsWebserverEnabled()) {
|
|
UpnpEnableWebserver(0);
|
|
if (UpnpIsWebserverEnabled()) {
|
|
PJ_LOG(4, (THIS_FILE, "Failed to disable web server"));
|
|
}
|
|
}
|
|
|
|
/* Makes the XML parser more tolerant to malformed text. */
|
|
ixmlRelaxParser(1);
|
|
|
|
upnp_mgr.initialized = 1;
|
|
upnp_mgr.primary_igd_idx = -1;
|
|
upnp_mgr.upnp_cb = param->upnp_cb;
|
|
upnp_mgr.pool = pj_pool_create(param->factory, "upnp", 512, 512, NULL);
|
|
if (!upnp_mgr.pool) {
|
|
pj_upnp_deinit();
|
|
return PJ_ENOMEM;
|
|
}
|
|
pj_mutex_create_recursive(upnp_mgr.pool, "upnp", &upnp_mgr.mutex);
|
|
|
|
ip_address = UpnpGetServerIpAddress();
|
|
port = UpnpGetServerPort();
|
|
#if PJ_HAS_IPV6
|
|
ip_address6 = UpnpGetServerIp6Address();
|
|
port6 = UpnpGetServerPort6();
|
|
#endif
|
|
if (param->if_name) {
|
|
PJ_LOG(4, (THIS_FILE, "UPnP initialized with interface %s",
|
|
param->if_name));
|
|
}
|
|
if (ip_address6 && port6) {
|
|
PJ_LOG(4, (THIS_FILE, "UPnP initialized on %s:%u (IPv4) and "
|
|
"%s:%u (IPv6)", ip_address, port,
|
|
ip_address6, port6));
|
|
} else {
|
|
PJ_LOG(4, (THIS_FILE, "UPnP initialized on %s:%u", ip_address, port));
|
|
}
|
|
|
|
/* Search for Internet Gateway Devices. */
|
|
upnp_mgr.status = PJ_EPENDING;
|
|
search_igd(param->search_time > 0? param->search_time:
|
|
PJ_UPNP_DEFAULT_SEARCH_TIME);
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
/* Deinitialize UPnP. */
|
|
PJ_DEF(pj_status_t) pj_upnp_deinit()
|
|
{
|
|
PJ_LOG(4, (THIS_FILE, "UPnP deinitializing..."));
|
|
|
|
/* Note that this function will wait until all its worker threads
|
|
* complete.
|
|
*/
|
|
UpnpFinish();
|
|
|
|
if (upnp_mgr.mutex)
|
|
pj_mutex_destroy(upnp_mgr.mutex);
|
|
|
|
if (upnp_mgr.pool)
|
|
pj_pool_release(upnp_mgr.pool);
|
|
|
|
pj_bzero(&upnp_mgr, sizeof(upnp_mgr));
|
|
upnp_mgr.primary_igd_idx = -1;
|
|
|
|
PJ_LOG(4, (THIS_FILE, "UPnP deinitialized"));
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|
|
/* Send request to add port mapping. */
|
|
PJ_DECL(pj_status_t)pj_upnp_add_port_mapping(unsigned sock_cnt,
|
|
const pj_sock_t sock[],
|
|
unsigned ext_port[],
|
|
pj_sockaddr mapped_addr[])
|
|
{
|
|
unsigned max_wait = 20;
|
|
unsigned i;
|
|
struct igd *igd = NULL;
|
|
pj_status_t status = PJ_SUCCESS;
|
|
|
|
if (!upnp_mgr.initialized) {
|
|
PJ_LOG(3, (THIS_FILE, "UPnP not initialized yet"));
|
|
return PJ_EINVALIDOP;
|
|
}
|
|
|
|
/* If IGD search hasn't completed, wait momentarily. */
|
|
while (upnp_mgr.status == PJ_EPENDING && max_wait > 0) {
|
|
pj_thread_sleep(100);
|
|
max_wait--;
|
|
}
|
|
|
|
/* Need to lock in case the device becomes offline at the same time. */
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
if (upnp_mgr.primary_igd_idx < 0) {
|
|
PJ_LOG(3, (THIS_FILE, "No valid IGD"));
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
return PJ_ENOTFOUND;
|
|
}
|
|
|
|
igd = &upnp_mgr.igd_devs[upnp_mgr.primary_igd_idx];
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
|
|
for (i = 0; i < sock_cnt; i++) {
|
|
static const char *ACTION_ADD_PORT_MAPPING = "AddPortMapping";
|
|
static const char *PORT_MAPPING_DESCRIPTION = "pjsip-upnp";
|
|
int upnp_err;
|
|
IXML_Document *action = NULL;
|
|
IXML_Document *response = NULL;
|
|
char int_port_buf[10], ext_port_buf[10];
|
|
char addr_buf[PJ_INET6_ADDRSTRLEN];
|
|
unsigned int_port;
|
|
pj_sockaddr bound_addr;
|
|
int namelen = sizeof(pj_sockaddr);
|
|
const char *pext_port = (ext_port? ext_port_buf: int_port_buf);
|
|
|
|
/* Get socket's bound address. */
|
|
status = pj_sock_getsockname(sock[i], &bound_addr, &namelen);
|
|
if (status != PJ_SUCCESS) {
|
|
PJ_LOG(3, (THIS_FILE, "getsockname() error"));
|
|
goto on_error;
|
|
}
|
|
|
|
if (!pj_sockaddr_has_addr(&bound_addr)) {
|
|
pj_sockaddr addr;
|
|
|
|
/* Get local IP address. */
|
|
status = pj_gethostip(bound_addr.addr.sa_family, &addr);
|
|
if (status != PJ_SUCCESS)
|
|
goto on_error;
|
|
|
|
pj_sockaddr_copy_addr(&bound_addr, &addr);
|
|
}
|
|
|
|
pj_sockaddr_print(&bound_addr, addr_buf, sizeof(addr_buf), 0);
|
|
int_port = pj_sockaddr_get_port(&bound_addr);
|
|
pj_utoa(int_port, int_port_buf);
|
|
if (ext_port)
|
|
pj_utoa(ext_port[i], ext_port_buf);
|
|
|
|
/* Create action XML. */
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewRemoteHost", "");
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewExternalPort", pext_port);
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewProtocol", "UDP");
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewInternalPort",int_port_buf);
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewInternalClient", addr_buf);
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewEnabled", "1");
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr,
|
|
"NewPortMappingDescription", PORT_MAPPING_DESCRIPTION);
|
|
UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING,
|
|
igd->service_type.ptr, "NewLeaseDuration","0");
|
|
|
|
/* Send the action XML. */
|
|
upnp_err = UpnpSendAction(upnp_mgr.client_hnd, igd->control_url.ptr,
|
|
igd->service_type.ptr, NULL, action,
|
|
&response);
|
|
if (upnp_err != UPNP_E_SUCCESS || !response) {
|
|
PJ_LOG(3, (THIS_FILE, "Failed to %s IGD %s to add port mapping "
|
|
"for %s:%s -> %s:%s: %d (%s)",
|
|
response? "send action to":
|
|
"get response from",
|
|
igd->dev_id.ptr,
|
|
igd->public_ip.ptr, pext_port,
|
|
addr_buf, int_port_buf, upnp_err,
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
status = PJ_ETIMEDOUT;
|
|
}
|
|
|
|
TRACE_("Add port mapping XML action:\n%s",
|
|
ixmlPrintDocument(action));
|
|
TRACE_("Add port mapping XML response:\n%s",
|
|
(response? ixmlPrintDocument(response): "empty"));
|
|
|
|
if (response && check_error_response(response)) {
|
|
/* The error detail will be printed by check_error_response(). */
|
|
status = PJ_EINVALIDOP;
|
|
}
|
|
|
|
ixmlDocument_free(action);
|
|
if (response) ixmlDocument_free(response);
|
|
|
|
pj_sockaddr_cp(&mapped_addr[i], &bound_addr);
|
|
pj_sockaddr_set_str_addr(bound_addr.addr.sa_family,
|
|
&mapped_addr[i], &igd->public_ip);
|
|
pj_sockaddr_set_port(&mapped_addr[i],
|
|
(pj_uint16_t)(ext_port? ext_port[i]: int_port));
|
|
|
|
if (status != PJ_SUCCESS)
|
|
goto on_error;
|
|
|
|
PJ_LOG(4, (THIS_FILE, "Successfully add port mapping to IGD %s: "
|
|
"%s:%s -> %s:%s", igd->dev_id.ptr,
|
|
igd->public_ip.ptr, pext_port,
|
|
addr_buf, int_port_buf));
|
|
}
|
|
|
|
return PJ_SUCCESS;
|
|
|
|
on_error:
|
|
/* Port mapping was unsuccessful, so we need to delete all
|
|
* the previous port mappings.
|
|
*/
|
|
while (i > 0) {
|
|
pj_upnp_del_port_mapping(&mapped_addr[--i]);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
/* Send request to delete port mapping. */
|
|
PJ_DEF(pj_status_t)pj_upnp_del_port_mapping(const pj_sockaddr *mapped_addr)
|
|
{
|
|
static const char* ACTION_DELETE_PORT_MAPPING = "DeletePortMapping";
|
|
int upnp_err;
|
|
struct igd *igd = NULL;
|
|
IXML_Document *action = NULL;
|
|
pj_status_t status = PJ_SUCCESS;
|
|
pj_sockaddr host_addr;
|
|
unsigned ext_port;
|
|
char ext_port_buf[10];
|
|
|
|
if (!upnp_mgr.initialized)
|
|
return PJ_EINVALIDOP;
|
|
|
|
/* Need to lock in case the device becomes offline at the same time. */
|
|
pj_mutex_lock(upnp_mgr.mutex);
|
|
if (upnp_mgr.primary_igd_idx < 0) {
|
|
PJ_LOG(3, (THIS_FILE, "No valid IGD"));
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
return PJ_ENOTFOUND;
|
|
}
|
|
|
|
igd = &upnp_mgr.igd_devs[upnp_mgr.primary_igd_idx];
|
|
pj_mutex_unlock(upnp_mgr.mutex);
|
|
|
|
/* Compare IGD's public IP to the mapped public address. */
|
|
pj_sockaddr_cp(&host_addr, mapped_addr);
|
|
pj_sockaddr_set_port(&host_addr, 0);
|
|
if (pj_sockaddr_cmp(&igd->public_ip_addr, &host_addr)) {
|
|
unsigned i;
|
|
|
|
/* The primary IGD's public IP is different. Find the IGD
|
|
* that matches the mapped address.
|
|
*/
|
|
igd = NULL;
|
|
for (i = 0; i < upnp_mgr.igd_cnt; i++, igd = NULL) {
|
|
igd = &upnp_mgr.igd_devs[i];
|
|
if (igd->valid && igd->alive &&
|
|
!pj_sockaddr_cmp(&igd->public_ip_addr, &host_addr))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!igd) {
|
|
/* Either the IGD we previously requested to add port mapping has become
|
|
* offline, or the address is actually not a valid.
|
|
*/
|
|
PJ_LOG(3, (THIS_FILE, "The IGD is offline or invalid mapped address"));
|
|
return PJ_EGONE;
|
|
}
|
|
|
|
ext_port = pj_sockaddr_get_port(mapped_addr);
|
|
if (ext_port == 0) {
|
|
/* Deleting port zero should be harmless, but it's a waste of time. */
|
|
PJ_LOG(3, (THIS_FILE, "Invalid port number to be deleted"));
|
|
return PJ_EINVALIDOP;
|
|
}
|
|
pj_utoa(ext_port, ext_port_buf);
|
|
|
|
/* Create action XML. */
|
|
UpnpAddToAction(&action, ACTION_DELETE_PORT_MAPPING, igd->service_type.ptr,
|
|
"NewRemoteHost", "");
|
|
UpnpAddToAction(&action, ACTION_DELETE_PORT_MAPPING, igd->service_type.ptr,
|
|
"NewExternalPort", ext_port_buf);
|
|
UpnpAddToAction(&action, ACTION_DELETE_PORT_MAPPING, igd->service_type.ptr,
|
|
"NewProtocol", "UDP");
|
|
|
|
/* For mapping deletion, send the action XML async, to avoid long
|
|
* wait in network disconnection scenario.
|
|
*/
|
|
upnp_err = UpnpSendActionAsync(upnp_mgr.client_hnd, igd->control_url.ptr,
|
|
igd->service_type.ptr, NULL, action,
|
|
client_cb, &upnp_mgr);
|
|
if (upnp_err == UPNP_E_SUCCESS) {
|
|
PJ_LOG(4, (THIS_FILE, "Successfully sending async action to "
|
|
"delete port mapping to IGD %s for "
|
|
"%s:%s", igd->dev_id.ptr,
|
|
igd->public_ip.ptr, ext_port_buf));
|
|
} else {
|
|
PJ_LOG(3, (THIS_FILE, "Failed to send action to IGD %s to delete "
|
|
"port mapping for %s:%s: %d (%s)",
|
|
igd->dev_id.ptr, igd->public_ip.ptr,
|
|
ext_port_buf, upnp_err,
|
|
UpnpGetErrorMessage(upnp_err)));
|
|
status = PJ_EINVALIDOP;
|
|
}
|
|
|
|
ixmlDocument_free(action);
|
|
|
|
return status;
|
|
}
|
|
|
|
#if defined(_MSC_VER)
|
|
# pragma comment(lib, "libupnp")
|
|
# pragma comment(lib, "libixml")
|
|
# pragma comment(lib, "libpthread")
|
|
#endif
|
|
|
|
#endif /* PJNATH_HAS_UPNP */
|