@achingbrain/ssdp
Version:
Yet another SSDP implementation for node.js
369 lines (292 loc) • 11.5 kB
Markdown
# @achingbrain/ssdp
[](https://codecov.io/gh/achingbrain/ssdp)
[](https://github.com/achingbrain/ssdp/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
> Yet another SSDP implementation for node.js
# About
<!--
!IMPORTANT!
Everything in this README between "# About" and "# Install" is automatically
generated and will be overwritten the next time the doc generator is run.
To make changes to this section, please update the @packageDocumentation section
of src/index.js or src/index.ts
To experiment with formatting, please run "npm run docs" from the root of this
repo and examine the changes made.
-->
First, import the module, call the function and set up an error handler:
```javascript
import ssdp from '@achingbrain/ssdp'
const bus = await ssdp()
// print error messages to the console
bus.on('error', console.error)
```
## Example - Find a service
Pass a `serviceType` to the `discover` method - when services are found events will be emitted:
```javascript
// this is the unique service name we are interested in:
const serviceType = 'urn:schemas-upnp-org:service:ContentDirectory:1'
for await (const service of bus.discover({ serviceType })) {
// search for instances of a specific service
}
bus.on('service:discover', service => {
// receive a notification about discovery of a service
})
bus.on('service:update', 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
})
```
## Example - Find all services
Don't pass any options to the `discover` method (n.b. you will also receive protocol related events):
```javascript
for await (const service of bus.discover()) {
// receive a notification about all service types
}
```
## Example - Advertise a service
```javascript
// advertise a service
const advert = await bus.advertise({
usn: 'urn:schemas-upnp-org:service:ContentDirectory:1',
details: {
URLBase: 'https://192.168.0.1:8001'
}
})
// stop advertising a service
await advert.stop()
```
For full options, see [lib/advertise/parse-options.js](lib/advertise/parse-options.js)
## Integrate with existing HTTP servers
By default when you create an advertisement an HTTP server is created to serve the `details.xml` document that describes your service. To use an existing server instead, do something like:
## Example - Hapi
```javascript
const advert = await 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'
}
})
server.route({
method: 'GET',
path: '/ssdp/details.xml',
handler: (request, reply) => {
reply(advert.service.details())
.type('text/xml')
}
})
```
## Example - Express
```javascript
const advert = await 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'
}
})
app.get('/ssdp/details.xml', async (request, response) => {
response.set('Content-Type', 'text/xml')
try {
const details = await advert.service.details()
response.send(details)
} catch (err) {
response.set('Content-Type', 'text/xml')
response.send(err)
}
})
```
## Example - Shutting down gracefully
`ssdp` opens several ports to communicate with other devices on your network, to shut them down, do something like:
```javascript
process.on('SIGINT',() => {
// 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(error => {
process.exit(error ? 1 : 0)
})
})
```
## Full API and options
```javascript
import ssdp from '@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 serviceType = 'urn:schemas-upnp-org:service:ContentDirectory:1'
// search for one type of service
for await (const service of bus.discover({ serviceType })) {
}
bus.on('service:discover', service => {
// receive a notification when a service of the passed type is discovered
})
bus.on('service:update', service => {
// receive a notification when that service is updated
})
// search for all types of service
for await (const service of bus.discover()) {
}
// advertise a service
const advert = await 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 USN
presentationURL: 'index.html'
}
}
})
// stop advertising a service
advert.stop()
```
## Device description document
During UPnP device discovery, clients can request a [description of the various capabilities your service offers](http://jan.newmarch.name/internetdevices/upnp/upnp-devices.html).
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`:
```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:
```javascript
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](https://libraries.io/npm/xml2js#user-content-xml-builder-usage)):
```javascript
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`.
## I want to see all protocol messages
No problem, try this:
```javascript
bus.on('transport:outgoing-message', (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', (message, remote) => {
console.info('<- Incoming from %s:%s', remote.address, remote.port)
console.info(message.toString('utf8'))
})
```
Alternatively see [test/fixtures/all.js](test/fixtures/all.js)
## References
- [LG SSDP discovery documentation](http://developer.lgappstv.com/TV_HELP/topic/lge.tvsdk.references.book/html/UDAP/UDAP/Discovery.htm)
- [UPnP overview](http://jan.newmarch.name/internetdevices/upnp/upnp.html)
- [UPnP device description](http://jan.newmarch.name/internetdevices/upnp/upnp-devices.html)
- [UPnP Device Architecture v1.1](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf)
- [diversario/node-ssdp](https://github.com/diversario/node-ssdp)
- [Xedecimal/node-ssdp](https://www.npmjs.com/package/ssdp) (no longer maintained)
# Install
```console
$ npm i @achingbrain/ssdp
```
# API Docs
- <https://achingbrain.github.io/ssdp>
# License
Licensed under either of
- Apache 2.0, ([LICENSE-APACHE](https://github.com/achingbrain/ssdp/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT ([LICENSE-MIT](https://github.com/achingbrain/ssdp/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
# Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.