libnpupnp  4.1.0
An almost compatible successor for the Portable UPnP reference library
libnpupnp Documentation

Table of Contents

Introduction

libpnupnp is a C++ reimplementation of the venerable libupnp, a.k.a. Portable UPnP* library. Most of the API is unchanged, except where libupnp exposed its internal XML ixml library DOM objects.

One principal objective of the evolution was to make the transition as easy as possible for a program currently using libupnp. The structure of the C API is conserved.

Beyond issues of reliability and maintability, the main evolution in the new library is the support for multiple network interfaces. Also, helper code for parsing an UPnP device description document and for accessing the machine network interfaces is now exposed.

libnupnp implements a low level interface to the UPnP protocol. It has a separate set of functions for client and device implementations, with a few common functions, mostly for initialization and termination.

This document is a high level overview, with references to the detailed documentation. It largely supposes that you have a working knowledge of UPnP concepts, and you will probably also need to read parts of the UPnP architecture standard.

We present the programming interface in three sections:

It is possible, but uncommon, to use both the client and device interfaces in the same instance.

Some code examples are present in the document. In addition, it may be useful to clone the npupnp samples repository, which contain buildable and runnable examples, and will also referenced further down.

Common functions

Configuring the message log

The library has an internal error/debug messages system with settable verbosity and output destination.

By default, only error messages will be logged, to stderr.

You can use the functions in upnpdebug.h to configure logging. This can be done before or after the main initialisation of the library, but is best done before, to ensure that the initialisation messages behave as desired.

Example:

#include <upnp/upnp.h>
#include <upnp/upnpdebug.h>
int main(int argc, char **argv)
{
UpnpSetLogFileNames("/tmp/mylogfilename.txt", "");
UpnpPrintf(UPNP_INFO, DOM, __FILE__, __LINE__, "my log message\n");
int success = UpnpInit2("", ...

Common library initialisation

The library initialization mostly consists in selecting one or several network interfaces and addresses, and starting a listener on them.

It should be noted that both client and device instanciations (which are identical at this point) start a network listener on the UPnP multicast UDP address and port, and on a TCP port specific to the library (49152 for the first instance by default).

The listener is used for UPnP discovery operations on both sides, for event reception on the client side, and for SOAP action request reception on the device side, and the operation of both sides is largely symmetric. In other words, UPnP clients also run a network server.

For historical and compatibility reasons there are several possible ways to initialize the library:

For the two last functions, the network adapter name can be specified as follows:

Example:

#include <upnp/upnp.h>
#include <upnp/upnpdebug.h>
#include <upnp/upnptools.h>
#include <iostream>
int main(int argc, char **argv)
{
if (argc != 2) {
std::cerr << "Usage: npupnp_init <ifname>\n";
return 1;
}
const char *ifname = argv[1];
int port = 0;
unsigned int flags = UPNP_FLAG_IPV6;
int success = UpnpInitWithOptions(
ifname, port, flags, UPNP_OPTION_NETWORK_WAIT, 5, UPNP_OPTION_END);
if (success != UPNP_E_SUCCESS) {
std::cerr << "init failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
return 1;
}
std::cout << "IPV4 address: " << UpnpGetServerIpAddress() << "\n";
std::cout << "IPV6 address: " << UpnpGetServerIp6Address() << "\n";
std::cout << "Port: " << UpnpGetServerPort() << "\n";
return 0;
}

Client side operation

Client: registration

A client (Control Point) program just needs to register its callback function to the library, and will immediately begin to receive network events (broadcast discovery messages):

int success = UpnpRegisterClient(mycallback, (void *)mycontext, &myhandle);

This register the Upnp_FunPtr mycallback function to receive events. The mycontext cookie will be passed with each call. The return value UpnpClient_Handle myhandle can be used to reference the session (e.g. to stop it with UpnpUnRegisterClient).

Client: discovery

A client could passively wait for network events to reveal the presence of UPnP devices on the network, but this might be rather slow, so you will normally want to trigger an UPnP search. This is done with the UpnpSearchAsync function.

int status = UpnpSearchAsync(hnd, windowsecs, "upnp:rootdevice", &mycontext);

The first parameter is the client handle obtained with UpnpRegisterClient.

The second one is an indication to the network devices of the maximum period of time within with they should respond (they will use a random delay within the window to avoid collisions between answers).

The third parameter is the UPnP SSDP search type. Check the UPnP standard for possible values.

The last parameter is the value which will be passed to the callback function.

The same function is used for all callbacks from the library. Only the calls relative to the search (e.g. UPNP_DISCOVERY_SEARCH_RESULT) will use the search context value. Calls relative, for example, to spontaneous advertisements (e.g. UPNP_DISCOVERY_ADVERTISEMENT_ALIVE) will use the generic client context.

When the search window is expired, the last call relative to the search will be of type UPNP_DISCOVERY_SEARCH_TIMEOUT, with a NULL event parameter.

The important value inside the Upnp_Discovery structure received by the callback function is the Location field, which contains the URL of the description document for the device. You can download this document with the UpnpDownloadUrlItem helper function.

std::string data;
std::string content_type;
success = UpnpDownloadUrlItem(url, data, content_type);

Once downloaded, the XML description document will need to be parsed to access the list of services and other information elements.

The following example uses the XML device description parser which comes with libnpupnp. This was not available in libupnp, so this sample is a bit farther from libupnp compatibility.

Note that, for a trivial program like the following, we could just pass nullptr for the context cookie addresses, which are not used.

The discovery results will usually be filtered by desired service availability, for presenting a choice to the user, or against a supplied device name.

#include <unistd.h>
#include <set>
#include <mutex>
#include <condition_variable>
#include <libupnpp/control/description.hxx>
#include <upnp/upnp.h>
#include <upnp/upnpdebug.h>
#include <upnp/upnptools.h>
#include <iostream>
static int clctxt;
std::set<std::string> locations;
std::mutex locations_mutex;
std::condition_variable locations_cv;
static bool searchdone;
static int mycallback(Upnp_EventType etyp, const void *evt, void *ctxt)
{
switch (etyp) {
goto dldesc;
break;
goto dldesc;
{
std::unique_lock<std::mutex> loclock;
searchdone = true;
locations_cv.notify_all();
}
break;
default:
std::cerr << "Received surprising event type " << etyp << "\n";
break;
}
return 0;
dldesc:
std::unique_lock<std::mutex> loclock;
locations.insert(dsp->Location);
locations_cv.notify_all();
return 0;
}
int main(int argc, char **argv)
{
if (success != UPNP_E_SUCCESS) {
std::cerr << "init failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
return 1;
}
success = UpnpRegisterClient(mycallback, &clctxt, &chdl);
if (success != UPNP_E_SUCCESS) {
std::cerr << "register client failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
return 1;
}
int searchctxt;
success = UpnpSearchAsync(chdl, 3, "upnp:rootdevice", &searchctxt);
std::unique_lock<std::mutex> locklocs(locations_mutex);
for (;;) {
locations_cv.wait(locklocs);
if (searchdone) {
break;
}
for (auto it = locations.begin(); it != locations.end();) {
std::string data;
std::string ct;
int success = UpnpDownloadUrlItem(*it, data, ct);
if (success != UPNP_E_SUCCESS) {
std::cerr << "UpnpDownloadUrlItem failed: " << success << " " <<
UpnpGetErrorMessage(success) << "\n";
continue;
}
UPnPClient::UPnPDeviceDesc desc(*it, data);
if (!desc.ok) {
std::cout << "Description parse failed for " << *it << "\n";
} else {
std::cout << "Got " << data.size() <<
" bytes of description data. " << " friendly name: " <<
desc.friendlyName << "\n";
}
it = locations.erase(it);
}
}
return 0;
}

Client: control

Among the interesting fields in the description document which was parsed above are the controlURL ones, which define for each service the endpoint for action messages.

Once this is determined, and if you know the actions and parameters supported by the service, you can use the UpnpSendAction call to request that the device does something for you:

status = UpnpSendAction(handle, headerString, actionURL, serviceType,
actionName, actionParams, responseData, errorCodep, errorDescr);

Refer to the UpnpSendAction reference documentation for details on the parameters, we will just add a few remarks here.

headerString is a value for the SOAP header, and is generally not needed and empty.

actionURL is the field named controlURL in the XML description document and its libupnpp parsed version.

serviceType also comes from the parsed description.

actionParams and responseData are the call arguments and return data as C++ vectors of name/value pairs. The arguments should be as defined by the service. For dynamic interface programs, this can be determined by downloading and parsing the service description document, the URL of which is found in the device description. However, for known services, you will usually just hard-code the argument list.

Client: events

A client can opt to be informed of the state changes in a service by subscribing to its events, using the UpnpSubscribe function.

int status = UpnpSubscribe(handle, eventURL, timeoutp, subs_id);

The eventURL value comes from the device description document.

timeoutp points to an integer containing the subscription timeout in seconds. This is the time in seconds, typically a fraction of an hour to a few hours, after which the device will cancel the subscription if it has not been renewed. The library can change the value passed by the client code.

The library performs automatic renewals of subscriptions internally, the client code does not need to deal with them.

subs_id is an Upnp_SID character array, which will contain the subscription identifier (a UUID) after the call. This value will be passed with the event callbacks, and can be used to cancel the subscription, using UpnpUnSubscribe.

Once the client is subscribed to the events fro a service, its callback function will be called with an event type of UPNP_EVENT_RECEIVED and a Upnp_Event data structure. The latter contains a map of the changed variables names and the new values.

The first callback, during or just after the UnpnSubscribe call will contain the name and values of all the service eventable state variables.

Device side operation

Device: registration

Registering a root device is done by calling the UpnpRegisterRootDevice2 function.

descriptionType, description, 0, 0, mycallback, mycontext, myhandle)

descriptionType is a Upnp_DescType member, and indicates if the description character pointer contains an URL, an inline character string, or a file name. In the two last cases, the description will be served from an internal buffer, with an URL path in the root of the WEB server tree. The file name will be the same as the input file name, or description.xml if the description is passed as a buffer.

Multiple root devices can be registered in a single instance of the library. You can't pass the description documents as memory buffers in this case (because of the fixed file name).

The two parameters after the description pointer are unused.

mycallback and mycontext are the callback and callback context parameter which will be used when the library calls the device code.

myhandle can be used to reference this device registration, for example for unsetting it with UpnpUnRegisterRootDevice

Device: discovery

UPnP device send advertisements about their presence when they start up, and later, at regular intervals. For a libnpupnp device, this process is started by a call to UpnpSendAdvertisement.

int status = UpnpSendAdvertisement(handle, expiration);

expiration is the time in seconds after which clients should consider the advertisement as expired. Set it to 0 to use the library default (1800 S). While the process lives, the library will automatically renew the advertisement before the expiration time, so you do not need to make further calls.

Without this call, the device will not advertise itself spontaneously on the network, but it will still answer search requests, so it is not invisible.

Device: WEB server and Virtual Directories

Device: WEB server: local file system

You can set the internal HTTP server to serve files from the local file system by calling UpnpSetWebServerRootDir.

int status = UpnpSetWebServerRootDir(rootDir);

If this is not called, the internal HTTP server will not allow any access to the local file system. In this case, you will need to set up a Virtual Directory to hold the service description files, which is actually the common way to do things.

The call will accept a relative rootDir parameter, but using one would be a bad idea in most cases.

See here for a small sample device storing its description files in the file system.

Device: Virtual Directories

The integrated WEB server has the capability to serve documents from application-provided data. This is performed by:

All subpaths of a defined virtual path are considered virtual, not only the immediate file children.

Virtual Paths are checked before file system paths and will hide the whole subtree (in case UpnpSetWebServerRootDir() has been called in a way which may result in collisions).

Virtual Directories: defining the callbacks

Let us see for example how to set the VDCallback_GetInfo function which is always the first one that the WEB server calls when processing a document request. The function prototype is as follows:

const char *filepath,
struct File_Info *info,
const void *cookie,
const void **request_cookiep
);

The filepath parameter is an absolute file system path.

The File_Info info pointer is used both to supply information about the HTTP request (its headers), and to return information about the file and possibly additional headers to set in the response.

cookie is an application context pointer set when defining the path as a Virtual Directory.

request_cookiep is specific of this request and can be set the VDCallback_GetInfo to track the different calls associated with this specific request.

The callback is defined through the following call:

int status = UpnpVirtualDir_set_GetInfoCallback(callback);

There are similar definitions for open, read, write (unused at the moment), seek and close.

It is also possible to set all the callbacks in one call by using the UpnpVirtualDirCallbacks structure and UpnpSetVirtualDirCallbacks().

Virtual Directories: defining a virtual path

You associate a path with the Virtual Directory system by using UpnpAddVirtualDir :

int status = UpnpAddVirtualDir(dirpath, cookie, oldcookiep);

The cookie will be set in further file-related calls under this path. The pointer-to-pointer oldcookiep can be set to retrieve the old value of the cookie in case the path was already associated. You can set it to nullptr if you're not interested.

You can also erase an association with UpnpRemoveVirtualDir, and get rid of them all with UpnpRemoveAllVirtualDirs.

See this file in the samples for an example of a small bogus but functional device using a virtual directory to store its description files.

The virtual directory implementation in the sample is actually file-system-backed to make things simple, but this does not change the principle.

Device: actions

Actions requested by a Control Point to a specific device service action will be received by the device callback with a type of UPNP_CONTROL_ACTION_REQUEST, and an associated Upnp_Action_Request data structure, used both to define what is requested, and the data which should be returned.

The main input fields of the structure are the following:

On output:

See this file in the samples for an example of a small bogus but functional device processing some actions.

Device: events

Device: events: subscription

A device implementation will be informed of a subscription request by a callback of type UPNP_EVENT_SUBSCRIPTION_REQUEST. The associated Upnp_Subscription_Request data structure holds the device and service identifiers, and the subscription ID set by the library.

The subscription can be rejected by returning an error, for example if there is an inconsistency in the device/service data, but will more commonly be accepted, in which case the initial set of state variable values should be returned by a call to UpnpAcceptSubscription or UpnpAcceptSubscriptionXML which only differ by the format of the returned state data.

This call should be performed from the UPNP_EVENT_SUBSCRIPTION_REQUEST callback.

int ret = UpnpAcceptSubscription(handle, UDN, serviceId, names, values, cnt, SID);
int ret UpnpAcceptSubscriptionXML(handle, UDN, serviceId, propertyset, SID);

UDN, serviceId, and SID come from the input Upnp_Subscription_Request structure.

names, values are parallel arrays of C string pointers, of size cnt.

Alternatively, propertyset is an XML GENA property set ready for sending back (see the UPnP standard).

Device: events: eventing

A device generates UPnP events by calls to UpnpNotify or UpnpNotifyXML, which only differ in the returned data format:

int ret = UpnpNotify(handle, UDN, serviceId, names, values, count);
int ret UpnpNotifyXML(handle, UDN, serviceId, propset)

The device application code only has to call one of these once for a state event. The library will arrange to send the event to all the currently suscribed control points.

Our bogus sample device also has eventing code, which can be triggered by it associated client using an action to change a variable value.