Yet another SSDP implementation for node.js
SSDP is a service discovery protocol that uses messages composed from HTTP-style headers sent over UDP. It fulfills a similar role to mDNS but needs no extra libraries and is implemented completely in JavaScript.
With SSDP a service will broadcast it's availability and respond to search messages over UDP and also present a description document that contains details of the capabilities it offers. A client can then search for available services and use them as required.
npm install @achingbrain/ssdp
First, import the module, call the function and set up an error handler:
var ssdp = require('@achingbrain/ssdp')
var bus = ssdp()
// print error messages to the console
bus.on('error', console.error)
Pass a usn
to the discover
method - when services are found events will be emitted:
// this is the unique service name we are interested in:
var usn = 'urn:schemas-upnp-org:service:ContentDirectory:1'
bus.discover(usn)
bus.on('discover:' + usn, function (service) {
// receive a notification about a service
bus.on('update:' + service.UDN, function (service) {
// receive a notification when that service is updated - nb. this will only happen
// after the service max-age is reached and if the service's device description
// document has changed
})
})
Don't pass any options to the discover
method (n.b. you will also receive protocol related events):
bus.discover()
bus.on('discover:*', function (service) {
// receive a notification about all service types
})
// advertise a service
bus.advertise({
usn: 'urn:schemas-upnp-org:service:ContentDirectory:1',
details: {
URLBase: 'https://192.168.0.1:8001'
}
})
.then(advert => {
// stop advertising a service
advert.stop()
})
For full options, see lib/advertise/parse-options.js
By default when you create an advertisment an HTTP server is created to serve the details.xml
document that describes your service. To use an existing server instead, do something like:
bus.advertise({
usn: 'urn:schemas-upnp-org:service:ContentDirectory:1',
location: {
udp4: 'http://192.168.0.1:8000/ssdp/details.xml'
},
details: {
URLBase: 'https://192.168.0.1:8001'
}
})
.then(advert => {
server.route({
method: 'GET',
path: '/ssdp/details.xml',
handler: (request, reply) => {
reply(advert.service.details())
.type('text/xml')
}
})
callback(error, server)
})
bus.advertise({
usn: 'urn:schemas-upnp-org:service:ContentDirectory:1',
location: {
udp4: 'http://192.168.0.1:8000/ssdp/details.xml'
},
details: {
URLBase: 'https://192.168.0.1:8001'
}
})
.then(advert => {
app.get('/ssdp/details.xml', (request, response) => {
advert.service.details()
.then(details => {
response.set('Content-Type', 'text/xml')
response.send(details)
})
.catch(error => {
response.set('Content-Type', 'text/xml')
response.send(error)
})
})
})
ssdp
opens several ports to communicate with other devices on your network, to shut them down, do something like:
process.on('SIGINT', function() {
// stop the server(s) from running - this will also send ssdp:byebye messages for all
// advertised services however they'll only have been sent once the callback is
// invoked so it won't work with process.on('exit') as you can only perform synchronous
// operations there
bus.stop(function (error) {
process.exit(error ? 1 : 0)
})
})
### Full API and options
var ssdp = require('@achingbrain/ssdp')
// all arguments are optional
var bus = ssdp({
udn: 'unique-identifier', // defaults to a random UUID
// a string to identify the server by
signature: 'node.js/0.12.6 UPnP/1.1 @achingbrain/ssdp/1.0.0',
retry {
times: 5, // how many times to attempt joining the UDP multicast group
interval: 5000 // how long to wait between attempts
},
// specify one or more sockets to listen on
sockets: [{
type: 'udp4', // or 'udp6'
broadcast: {
address: '239.255.255.250', // or 'FF02::C'
port: 1900 // SSDP broadcast port
},
bind: {
address: '0.0.0.0', // or '0:0:0:0:0:0:0:0'
port: 1900
},
maxHops: 4 // how many network segments packets are allow to travel through (UDP TTL)
}]
})
bus.on('error', console.error)
// this is the type of service we are interested in
var usn = 'urn:schemas-upnp-org:service:ContentDirectory:1'
// search for one type of service
bus.discover(usn)
bus.on('discover:' + usn, function (service) {
// receive a notification when a service of the passed type is discovered
bus.on('update:' + service.device.UDN, function (service) {
// receive a notification when that service is updated
})
})
// search for all types of service
bus.discover()
bus.on('discover:*', function (service) {
// receive a notification about all discovered services
})
// advertise a service
bus.advertise({
usn: 'a-usn', // unique service name
interval: 10000, // how often to broadcast service adverts in ms
ttl: 1800000, // how long the advert is valid for in ms
ipv4: true, // whether or not to broadcast the advert over IPv4
ipv6: true, // whether or not to broadcast the advert over IPv6
location: { // where the description document(s) are available - omit to have an http server automatically created
udp4: 'http://192.168.0.1/details.xml', // where the description document is available over ipv4
udp6: 'http://FE80::0202:B3FF:FE1E:8329/details.xml' // where the description document is available over ipv6
},
details: { // the contents of the description document
specVersion: {
major: 1,
minor: 1
},
URLBase: 'http://example.com',
device: {
deviceType: 'a-usn',
friendlyName: 'A friendly device name',
manufacturer: 'Manufactuer name',
manufacturerURL: 'http://example.com',
modelDescription: 'A description of the device',
modelName: 'A model name',
modelNumber: 'A vendor specific model number',
modelURL: 'http://example.com',
serialNumber: 'A device specific serial number',
UDN: 'unique-identifier' // should be the same as the bus UDN
presentationURL: 'index.html'
}
}
})
.then(advert => {
// stop advertising a service
advert.stop()
})
During UPnP device discovery, clients can request a description of the various capabilities your service offers.
To do this you can either store an xml document and set the location
field of your advert to point at that document
or have it automatically generated.
E.g., create a document, description.xml
and put it on a server at http://server.com/path/to/description.xml
:
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://192.168.1.41:80</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>I am a light controller</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<manufacturerURL>http://www.philips.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2012</modelName>
<modelNumber>23409823049823</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>asd09f8s90832</serialNumber>
<UDN>uuid:2f402f80-da50-12321-9b23-2131298129</UDN>
<presentationURL>index.html</presentationURL>
</device>
</root>
Then create your advert:
bus.advertise({
usn: 'urn:schemas-upnp-org:device:Basic:1',
location: {
udp4: 'http://192.168.1.40/path/to/description.xml'
}
})
Alternatively provide an descriptor object and let this module do the heavy lifting (n.b. your object will be run through the xml2js Builder):
bus.advertise({
usn: 'urn:schemas-upnp-org:device:Basic:1',
details: {
'$': {
'xmlns': 'urn:schemas-upnp-org:device-1-0'
},
'specVersion': {
'major': '1',
'minor': '0'
},
'URLBase': 'http://192.168.1.41:80',
'device': {
'deviceType': 'urn:schemas-upnp-org:device:Basic:1',
'friendlyName': 'I am a light controller',
'manufacturer': 'Royal Philips Electronics',
'manufacturerURL': 'http://www.philips.com',
'modelDescription': 'Philips hue Personal Wireless Lighting',
'modelName': 'Philips hue bridge 2012',
'modelNumber': '23409823049823',
'modelURL': 'http://www.meethue.com',
'serialNumber': 'asd09f8s90832',
'UDN': 'uuid:2f402f80-da50-12321-9b23-2131298129',
'presentationURL': 'index.html'
}
}
})
A random high port will be chosen, a http server will listen on that port and serve the descriptor and the LOCATION
header will be set appropriately in all ssdp
messages.
The server will be shut down when you call advert.stop
.
No problem, try this:
bus.on('transport:outgoing-message', function (socket, message, remote) {
console.info('-> Outgoing to %s:%s via %s', remote.address, remote.port, socket.type)
console.info(message.toString('utf8'))
})
bus.on('transport:incoming-message', function (message, remote) {
console.info('<- Incoming from %s:%s', remote.address, remote.port)
console.info(message.toString('utf8'))
})
Alternatively see test/fixtures/all.js