Writing a UPnP Control Point in JavaScript, Part Two

Published on

While writing one of my Node.js packages, I had my first opportunity to work with UPnP. Networking and communication using lower-level network protocols isn’t something that I usually do as a web developer. In this second part, we’ll look at how to implement SSDP over UDP, the second piece in solving the Discovery step of UPnP.

If you’re new to this series, or you’re looking for a specific part, have a look at the link(s) below:

  1. Part One, Communicating over UDP
  2. Part Two, Implementing SSDP over UDP

Intro to SSDP

SSDP, or Simple Service Discovery Protocol, is a text-based messaging protocol on top of UDP. It defines how UPnP devices and control points advertise their presence and discover one another.

SSDP messages are sent to a designated IP multicast address on UDP port 1900. For IPv4, the multicast address is 239.255.255.250. For IPv6, the address is one of:

  • ff02::C - link-local
  • ff05::C - site-local (equivalent to the IPv4 address)
  • ff08::C - organization-local
  • ff0E::C - global

Whenever the status of a UPnP device on the network changes or needs to be refreshed, it will send out a NOTIFY message to the appropriate multicast address and port. This NOTIFY message contains information about what type of notification it is, as well as information about the device that sent the message.

From the perspective of a control point, every device has a TTL, or Time to Live, measured in seconds - usually at least 1800 seconds, equal to 30 minutes. Once that TTL has elapsed, the control point should assume that the device is no longer available. For this reason, UPnP devices will send out periodic NOTIFY messages with the type of ssdp:alive in order to refresh the TTL.

Below, you can see an example ssdp:alive notification that gets sent from my Sonos Play:3.

NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age = 1800
LOCATION: http://192.168.2.4:1400/xml/device_description.xml
NT: upnp:rootdevice
NTS: ssdp:alive
SERVER: Linux UPnP/1.0 Sonos/29.5-91030 (ZPS3)
USN: uuid:RINCON_B8E937D8315401400::upnp:rootdevice
X-RINCON-HOUSEHOLD: Sonos_bKHBLpRUbhAoPhKahnvUPNfJn3
X-RINCON-BOOTSEQ: 5

When a UPnP device leaves the network or becomes unavailable, it sends out a similar message with the type ssdp:byebye.

Discovery

While it’s great that these NOTIFY messages are sent out periodically, a control point needs to see what UPnP devices are available on the network immediately. Waiting for up to 30 minutes for a NOTIFY message just isn’t going to work.

SSDP provides us the ability to perform a search of UPnP devices on the network. This is accomplished by sending out an M-SEARCH message to the appropriate multicast address and port. All UPnP devices on the network should be listening for M-SEARCH messages and will respond back to the control point with a message containing information about itself.

An example of an M-SEARCH message and a response is below.

M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 3
ST: upnp:rootdevice
HTTP/1.1 200 OK
CACHE-CONTROL: max-age = 1800
EXT:
LOCATION: http://192.168.2.4:1400/xml/device_description.xml
SERVER: Linux UPnP/1.0 Sonos/29.5-91030 (ZPS3)
ST: upnp:rootdevice
USN: uuid:RINCON_B8E937D8315401400::upnp:rootdevice
X-RINCON-HOUSEHOLD: Sonos_bKHBLpRUbhAoPhKahnvUPNfJn3
X-RINCON-BOOTSEQ: 5

There are two things that I want to point out in the M-SEARCH message. The MX field is the length of time in seconds that devices have to respond. Usually, 3 seconds is more than enough, but this can be increased or decreased. We also have the ST, or “Search Target”, field. By passing in upnp:rootdevice here, we’re putting out a search only for devices, and not device services, as well.

In the response, the UPnP device includes the location of an XML file. This XML file contains detailed information about the device and its services. This XML file will also have URLs to other XML files that describe the SOAP API for each of the services on the device.

Implementation

So, how do we do this in JavaScript? To start, lets try listening for NOTIFY messages. As we went over in part one, we will create a UDP socket with the dgram module.

import dgram from 'dgram';

const socket = dgram.createSocket('udp4');
socket.bind(1900);

Because we’re listening for multicast messages, we can’t just bind our socket to any random port. The multicast port is 1900, so we’ve bound our port to 1900.

Now that we have our UDP socket, we need to be able to listen for messages that are sent to the multicast address. Once our socket is bound and listening, we can add our membership to the multicast address and start receiving messages.

import dgram from 'dgram';

const socket = dgram.createSocket('udp4');
socket.on('listening', () => {
    socket.addMembership('239.255.255.250');
});
socket.on('message', (message) => {
    console.log(message.toString()); // messages are Buffers, remember?
});
socket.bind(1900);

Now we’re receiving all UDP multicast messages that are sent over the network, including M-SEARCH and NOTIFY messages. To perform a search for devices, we can use the same socket (or create a new one) and send a message that follows the M-SEARCH formatting.

import dgram from 'dgram';

const search = new Buffer([
    'M-SEARCH * HTTP/1.1',
    'HOST: 239.255.255.250:1900',
    'MAN: "ssdp:discover"',
    'MX: 3',
    'ST: upnp:rootdevice'
].join('\r\n'));

const socket = dgram.createSocket('udp4');
socket.on('listening', () => {
    socket.addMembership('239.255.255.250');
    socket.send(search, 0, search.length, 1900, 239.255.255.250);
});
socket.on('message', (message) => {
    console.log(message.toString());
});
socket.bind(1900);

Just like that, we’re able to search for devices on the network, and then continue listening for updates to those devices (and device services). Try playing around with the code above in babel-node.

What’s Next

So, we’ve got SSDP working and devices are sending us their location, as well as a description about their functions (see the LOCATION key from the M-SEARCH response above). Usually the device descriptions contain information about how to control their various functions through SOAP requests. From here on, we can use http to send SOAP requests to the device and control it!


Verify the signed markdown version of this article:

curl https://keybase.io/sethlopez/key.asc | gpg --import && \
curl https://sethlopez.me/article/writing-a-upnp-control-point-in-javascript-part-two/ | gpg