├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTORS.txt ├── Makefile ├── README.md ├── example ├── client.js └── server.js ├── index.js ├── lib ├── client.js ├── index.js └── server.js ├── package.json └── test ├── helper.js └── lib ├── client.js ├── index.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | *~ 3 | *.csv 4 | .DS_* 5 | node_modules/* 6 | !node_modules/fc.lib.*/ 7 | atlassian-ide-plugin.xml 8 | .idea/ 9 | *.log 10 | .settings 11 | node_modules 12 | tools 13 | npm-debug.log 14 | coverage/ 15 | .coveralls.yml 16 | reports/ 17 | nbproject 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .settings 2 | .project 3 | /node_modules 4 | .coveralls.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | sudo: false 5 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Jesus Dario , 2 | Luis Pinto , 3 | Fran Rios , 4 | Pablo Pizarro , 5 | Rajesh Sivanesan , 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = spec 2 | 3 | test: 4 | @mocha --recursive --reporter $(REPORTER) 5 | 6 | coverage: 7 | @$(MAKE) clean 8 | @mkdir reports 9 | @node_modules/.bin/istanbul instrument --output lib-cov lib 10 | @ISTANBUL_REPORTERS=lcov SSDP_COV=1 node_modules/.bin/mocha --recursive -R mocha-istanbul -t 20s $(TESTS) 11 | @mv lcov.info reports 12 | @mv lcov-report reports 13 | @rm -rf lib-cov coverage 14 | 15 | coveralls: test coverage 16 | @cat reports/lcov.info | ./node_modules/.bin/coveralls 17 | @$(MAKE) clean 18 | 19 | clean: 20 | @rm -rf lib-cov lcov-report reports lcov.info 21 | 22 | .PHONY: test test-cov coverage 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSDP fork for React Native from node 2 | 3 | This is a fork of `node-ssdp` that uses `react-native-udp` instead of `dgram` to enable react multicast messaging and plain socket control. The API is the same as in the forked version. 4 | 5 | 6 | works with yeti 7 | 8 | 9 | > This package powers [Yeti Smart Home](https://getyeti.co) and is used in production. It is maintained with our developers's free time, PRs and issues are more than welcome. 10 | 11 | ## Installation 12 | 13 | Unless React Native Version is > 0.29 use rnpm else use react-native link. 14 | 15 | ```javascript 16 | npm install react-native-ssdp 17 | rnpm link 18 | ``` 19 | 20 | Make sure you set `Buffer` as global in `react-native-udp`'s UdpSockets.js (noted filename end with letter s) 21 | 22 | ```javascript 23 | global.Buffer = global.Buffer || require('buffer').Buffer 24 | ``` 25 | 26 | ## Things to take note 27 | - Be aware that the Android emulator does not support incoming UDP traffic, so the client will not get any responses to search requests. For best results, test on a real device when using Android. The iOS simulator, on the other hand, works just fine. 28 | 29 | - Make sure you close all the sockets you open and never try to reopen one already open, mobile OSs and React Native are very aggresive both with security and performance, so a misuse could kill your process. 30 | 31 | ## Usage (Android) 32 | 33 | ### android/settings.gradle 34 | 35 | ```gradle 36 | ... 37 | include ':react-native-udp' 38 | project(':react-native-udp').projectDir = new File(settingsDir, '../node_modules/react-native-udp/android') 39 | include ':react-native-network-info' 40 | project(':react-native-network-info').projectDir = new File(settingsDir, '../node_modules/react-native-network-info/android') 41 | ``` 42 | 43 | ### android/app/build.gradle 44 | 45 | ```gradle 46 | ... 47 | dependencies { 48 | ... 49 | compile project(':react-native-udp') 50 | compile project(':react-native-network-info') 51 | } 52 | ``` 53 | 54 | ### register module in MainActivity.java 55 | 56 | #### For RN 0.19.0 and higher 57 | ```java 58 | import com.tradle.react.UdpSocketsModule; // <--- import 59 | import com.pusherman.networkinfo.RNNetworkInfoPackage; // <--- import 60 | 61 | public class MainActivity extends ReactActivity { 62 | // ... 63 | @Override 64 | protected List getPackages() { 65 | return Arrays.asList( 66 | new MainReactPackage(), // <---- add comma 67 | new UdpSocketsModule(), // <---- add package 68 | new RNNetworkInfoPackage() // <---------- add package 69 | ); 70 | } 71 | ``` 72 | 73 | #### For react-native 0.29.0 and higher ( in MainApplication.java ) 74 | ```java 75 | import com.tradle.react.UdpSocketsModule; // <--- import 76 | import com.pusherman.networkinfo.RNNetworkInfoPackage; // <--- import 77 | 78 | public class MainApplication extends Application implements ReactApplication { 79 | // ... 80 | @Override 81 | protected List getPackages() { 82 | return Arrays.asList( 83 | new MainReactPackage(), // <---- add comma 84 | new UdpSocketsModule(), // <---- add package 85 | new RNNetworkInfoPackage() <---- add package 86 | ); 87 | } 88 | ``` 89 | 90 | ## Usage (iOS) 91 | 92 | ### Adding with CocoaPads 93 | 94 | Add the RNFS pod to your list of applications pods in your podfile, using the path from the Podfile to the installed module. 95 | 96 | ``` 97 | pod 'RNUDP', :path => './node_modules/react-native-udp' 98 | pod 'RNNetworkInfo', :path => './node_modules/react-native-network-info' 99 | ``` 100 | 101 | Install pods as usual: 102 | ``` 103 | pod install 104 | ``` 105 | 106 | ### Adding manually in Xcode 107 | 108 | In XCode, in the project navigator, right click Libraries ➜ Add Files to [your project's name] Go to node_modules ➜ react-native-fs and add the .xcodeproj file 109 | 110 | In XCode, in the project navigator, select your project. Add the `lib*.a` from the RNFS project to your project's Build Phases ➜ Link Binary With Libraries. Click the .xcodeproj file you added before in the project navigator and go the Build Settings tab. Make sure 'All' is toggled on (instead of 'Basic'). Look for Header Search Paths and make sure it contains both `$(SRCROOT)/../react-native/React` and `$(SRCROOT)/../../React` - mark both as recursive. 111 | 112 | Run your project (Cmd+R) 113 | 114 | **Note:** Library uses these installed native_modules internally for sockets communication. 115 | 116 | ## Usage - Client 117 | 118 | ```javascript 119 | var Client = require('react-native-ssdp').Client 120 | , client = new Client(); 121 | 122 | client.on('response', function (headers, statusCode, rinfo) { 123 | console.log('Got a response to an m-search.'); 124 | }); 125 | 126 | // search for a service type 127 | client.search('urn:schemas-upnp-org:service:ContentDirectory:1'); 128 | 129 | // Or get a list of all services on the network 130 | 131 | client.search('ssdp:all'); 132 | ``` 133 | 134 | ## Usage - Server 135 | 136 | ```javascript 137 | var Server = require('react-native-ssdp').Server 138 | , server = new Server() 139 | ; 140 | 141 | server.addUSN('upnp:rootdevice'); 142 | server.addUSN('urn:schemas-upnp-org:device:MediaServer:1'); 143 | server.addUSN('urn:schemas-upnp-org:service:ContentDirectory:1'); 144 | server.addUSN('urn:schemas-upnp-org:service:ConnectionManager:1'); 145 | 146 | server.on('advertise-alive', function (headers) { 147 | // Expire old devices from your cache. 148 | // Register advertising device somewhere (as designated in http headers heads) 149 | }); 150 | 151 | server.on('advertise-bye', function (headers) { 152 | // Remove specified device from cache. 153 | }); 154 | 155 | // start the server 156 | server.start(); 157 | 158 | process.on('exit', function(){ 159 | server.stop() // advertise shutting down and stop listening 160 | }) 161 | ``` 162 | 163 | 164 | # Legacy docs from the forked repo 165 | 166 | Take a look at `example` directory as well to see examples or client and server. 167 | 168 | ##Configuration 169 | `new SSDP([options, [socket]])` 170 | 171 | SSDP constructor accepts an optional configuration object and an optional initialized socket. At the moment, the following is supported: 172 | 173 | - `ssdpSig` _String_ SSDP signature. Default: `node.js/NODE_VERSION UPnP/1.1 node-ssdp/PACKAGE_VERSION` 174 | - `ssdpIp` _String_ SSDP multicast group. Default: `239.255.255.250`. 175 | - `ssdpPort` _Number_ SSDP port. Default: `1900` 176 | - `ssdpTtl` _Number_ Multicast TTL. Default: `1` 177 | - `adInterval` _Number_ `advertise` event frequency (ms). Default: 10 sec. 178 | - `unicastHost` _String_ IP address or hostname of server where SSDP service is running. This is used in `HOST` header. Default: `0.0.0.0`. 179 | - `location` _String_ URL pointing to description of your service, or a function which returns that URL 180 | - `udn` _String_ Unique Device Name. Default: `uuid:f40c2981-7329-40b7-8b04-27f187aecfb5`. 181 | - `description` _String_ Path to description file. Default: `upnp/desc.php`. 182 | - `ttl` _Number_ Packet TTL. Default: `1800`. 183 | - `allowWildcards` _Boolean_ Accept wildcards (`*`) in `serviceTypes` of `M-SEARCH` packets, e.g. `usn:Belkin:device:**`. Default: `false` 184 | 185 | ###Logging 186 | 187 | You can enable logging via an environment variable `DEBUG`. Set `DEBUG=node-ssdp*` to enable all logs. To enable only client or server logs, use 188 | `DEBUG=node-ssdp:client` or `DEBUG=node-ssdp:server` respectively. 189 | 190 | # License 191 | 192 | (The MIT License) 193 | 194 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 195 | 196 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 197 | 198 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 199 | -------------------------------------------------------------------------------- /example/client.js: -------------------------------------------------------------------------------- 1 | var ssdp = require('../index').Client 2 | , client = new ssdp({ 3 | // unicastHost: '192.168.11.63' 4 | }) 5 | 6 | client.on('notify', function () { 7 | //console.log('Got a notification.') 8 | }) 9 | 10 | client.on('response', function inResponse(headers, code, rinfo) { 11 | console.log('Got a response to an m-search:\n%d\n%s\n%s', code, JSON.stringify(headers, null, ' '), JSON.stringify(rinfo, null, ' ')) 12 | }) 13 | 14 | client.search('urn:schemas-upnp-org:service:ContentDirectory:1') 15 | 16 | // Or maybe if you want to scour for everything after 5 seconds 17 | setTimeout(function() { 18 | client.search('ssdp:all') 19 | }, 5000) 20 | 21 | // And after 10 seconds, you want to stop 22 | setTimeout(function () { 23 | client.stop() 24 | }, 10000) 25 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var SSDP = require('../index').Server 2 | , server = new SSDP({ 3 | //unicastHost: '192.168.11.63', 4 | location: require('ip').address() + '/desc.html' 5 | }) 6 | 7 | server.addUSN('upnp:rootdevice') 8 | server.addUSN('urn:schemas-upnp-org:device:MediaServer:1') 9 | server.addUSN('urn:schemas-upnp-org:service:ContentDirectory:1') 10 | server.addUSN('urn:schemas-upnp-org:service:ConnectionManager:1') 11 | 12 | server.on('advertise-alive', function (heads) { 13 | //console.log('advertise-alive', heads) 14 | // Expire old devices from your cache. 15 | // Register advertising device somewhere (as designated in http headers heads) 16 | }) 17 | 18 | server.on('advertise-bye', function (heads) { 19 | //console.log('advertise-bye', heads) 20 | // Remove specified device from cache. 21 | }) 22 | 23 | // start server on all interfaces 24 | server.start('0.0.0.0') 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = process.env.SSDP_COV ? './lib-cov/' : '' 2 | 3 | module.exports = { 4 | Client: require('./lib/client'), 5 | Base: require('./lib/index') 6 | } 7 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var SSDP = require('./') 2 | var Buffer = require('buffer/').Buffer 3 | 4 | /** 5 | * 6 | * @param opts 7 | * @param [sock] 8 | * @constructor 9 | */ 10 | class SsdpClient extends SSDP { 11 | constructor (opts, sock) { 12 | super(opts, sock) 13 | this._subclass = 'node-ssdp:client' 14 | } 15 | } 16 | 17 | /** 18 | * 19 | * @param [cb] 20 | */ 21 | SsdpClient.prototype.start = function (cb) { 22 | this._start(0, this._unicastHost, cb) 23 | } 24 | 25 | /** 26 | *Close UDP socket. 27 | */ 28 | SsdpClient.prototype.stop = function () { 29 | if (!this.sock) { 30 | this._logger('Already stopped.') 31 | return 32 | } 33 | 34 | this._stop() 35 | } 36 | 37 | /** 38 | * 39 | * @param {String} serviceType 40 | * @returns {*} 41 | */ 42 | SsdpClient.prototype.search = function search(serviceType) { 43 | var self = this 44 | 45 | if (!this._started) { 46 | return this.start(function () { 47 | self.search(serviceType) 48 | }) 49 | } 50 | 51 | var pkt = self._getSSDPHeader( 52 | 'M-SEARCH', 53 | { 54 | 'HOST': self._ssdpServerHost, 55 | 'ST': serviceType, 56 | 'MAN': '"ssdp:discover"', 57 | 'MX': 3 58 | } 59 | ) 60 | 61 | self._logger('Sending an M-SEARCH request') 62 | 63 | var message = new Buffer(pkt) 64 | 65 | self._send(message, function (err, bytes) { 66 | self._logger('Sent M-SEARCH request: %o', {'message': pkt}) 67 | }) 68 | } 69 | 70 | module.exports = SsdpClient 71 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var dgram = require('react-native-udp') 4 | , EE = require('events').EventEmitter 5 | , debug = require('debug') 6 | 7 | var httpHeader = /HTTP\/\d{1}\.\d{1} \d+ .*/ 8 | , ssdpHeader = /^([^:]+):\s*(.*)$/ 9 | 10 | var nodeVersion = '6.0.0' 11 | , moduleVersion = require('../package.json').version 12 | , moduleName = require('../package.json').name 13 | 14 | var { NetworkInfo } = require('react-native-network-info') 15 | 16 | var Buffer = require('buffer/') 17 | /** 18 | * Options: 19 | * 20 | * @param {Object} opts 21 | * @param {String} opts.ssdpSig SSDP signature 22 | * @param {String} opts.ssdpIp SSDP multicast group 23 | * @param {String} opts.ssdpPort SSDP port 24 | * @param {Number} opts.ssdpTtl Multicast TTL 25 | * @param {Number} opts.adInterval Interval at which to send out advertisement (ms) 26 | * @param {String} opts.description Path to SSDP description file 27 | * @param {String} opts.udn SSDP Unique Device Name 28 | * 29 | * @param {Number} opts.ttl Packet TTL 30 | * @param {Boolean} opts.allowWildcards Allow wildcards in M-SEARCH packets (non-standard) 31 | * 32 | * @returns {SSDP} 33 | * @constructor 34 | */ 35 | class SSDP extends EE { 36 | constructor(opts, sock) { 37 | super() 38 | 39 | if (!(this instanceof SSDP)) return new SSDP(opts) 40 | 41 | this._subclass = this._subclass || 'ssdp-base' 42 | 43 | // we didn't get options, only socket 44 | if (!sock) { 45 | if (opts && /^udp\d$/.test(opts.type) && typeof opts.addMembership == 'function') { 46 | sock = opts 47 | opts = null 48 | } 49 | } 50 | 51 | opts = opts || {} 52 | 53 | if (sock) { 54 | this.sock = sock 55 | } else { 56 | this.sock = dgram.createSocket('udp4') 57 | this.sock.unref() 58 | } 59 | 60 | this.sock.on('close', () => { 61 | this.emit('close', this.sock) 62 | }) 63 | 64 | this._init(opts) 65 | } 66 | } 67 | 68 | //util.inherits(SSDP, EE) 69 | 70 | /** 71 | * Initializes instance properties. 72 | * @param opts 73 | * @private 74 | */ 75 | SSDP.prototype._init = function (opts) { 76 | this._logger = debug(this._subclass) 77 | 78 | this._ssdpSig = opts.ssdpSig || getSsdpSignature() 79 | 80 | // User shouldn't need to set these 81 | this._ssdpIp = opts.ssdpIp || '239.255.255.250' 82 | this._ssdpPort = opts.ssdpPort || 1900 83 | this._ssdpTtl = opts.ssdpTtl || 1 84 | 85 | this._adInterval = opts.adInterval || 10000 86 | 87 | this._ttl = opts.ttl || 1800 88 | 89 | if (typeof opts.location === 'function') { 90 | Object.defineProperty(this, '_location', { 91 | enumerable: true, 92 | get: opts.location 93 | }) 94 | } else { 95 | NetworkInfo.getIPAddress(ip => { 96 | this._location = opts.location || 'http://' + ip + ':' + 10293 + '/upnp/desc.html' 97 | }) 98 | } 99 | 100 | this._unicastHost = opts.unicastHost || '0.0.0.0' 101 | this._ssdpServerHost = this._ssdpIp + ':' + this._ssdpPort 102 | 103 | this._usns = {} 104 | this._udn = opts.udn || 'uuid:f40c2981-7329-40b7-8b04-27f187aecfb5' 105 | 106 | this._allowWildcards = opts.allowWildcards 107 | } 108 | 109 | /** 110 | * Advertise shutdown and close UDP socket. 111 | */ 112 | SSDP.prototype._stop = function () { 113 | if (!this.sock) { 114 | this._logger('Already stopped.') 115 | return 116 | } 117 | 118 | this.sock.close() 119 | this.sock = null 120 | 121 | this._socketBound = this._started = false 122 | } 123 | 124 | 125 | /** 126 | * Configures UDP socket `socket`. 127 | * Binds event listeners. 128 | */ 129 | SSDP.prototype._start = function (port, host, cb) { 130 | var self = this 131 | 132 | if (self._started) { 133 | self._logger('Already started.') 134 | return 135 | } 136 | 137 | self._started = true 138 | 139 | this.sock.addListener('error', function onSocketError(err) { 140 | self._logger('Socker error: %s', err.message) 141 | }) 142 | 143 | this.sock.addListener('message', function onSocketMessage(msg, rinfo) { 144 | self._parseMessage(msg, rinfo) 145 | }) 146 | 147 | this.sock.addListener('listening', function onSocketListening() { 148 | var addr = self.sock.address() 149 | 150 | self._logger('SSDP listening: %o', { address: 'http://' + addr.address + ':' + addr.port }) 151 | 152 | addMembership() 153 | 154 | function addMembership() { 155 | try { 156 | // The following line prevents Android 7 from closing the socket 157 | // this.socket.setBroadcast(true); 158 | self.sock.addMembership(self._ssdpIp) 159 | self.sock.setMulticastTTL(self._ssdpTtl) 160 | } catch (e) { 161 | if (e.code === 'ENODEV' || e.code === 'EADDRNOTAVAIL') { 162 | self._logger('No interface present to add multicast group membership. Scheduling a retry. %s', e.message) 163 | setTimeout(addMembership, 5000) 164 | } else { 165 | throw e 166 | } 167 | } 168 | } 169 | }) 170 | 171 | this.sock.bind(port, host, cb) 172 | } 173 | 174 | /** 175 | * Routes a network message to the appropriate handler. 176 | * 177 | * @param msg 178 | * @param rinfo 179 | */ 180 | SSDP.prototype._parseMessage = function (msg, rinfo) { 181 | msg = msg.toString() 182 | 183 | //this._logger('Multicast message: %o', {message: msg}) 184 | 185 | var type = msg.split('\r\n').shift() 186 | 187 | // HTTP/#.# ### Response to M-SEARCH 188 | if (httpHeader.test(type)) { 189 | this._parseResponse(msg, rinfo) 190 | } else { 191 | this._parseCommand(msg, rinfo) 192 | } 193 | } 194 | 195 | 196 | /** 197 | * Parses SSDP command. 198 | * 199 | * @param msg 200 | * @param rinfo 201 | */ 202 | SSDP.prototype._parseCommand = function parseCommand(msg, rinfo) { 203 | var method = this._getMethod(msg) 204 | , headers = this._getHeaders(msg) 205 | 206 | switch (method) { 207 | case 'NOTIFY': 208 | this._notify(headers, msg, rinfo) 209 | break 210 | case 'M-SEARCH': 211 | this._msearch(headers, msg, rinfo) 212 | break 213 | default: 214 | this._logger('Unhandled command: %o', { 'message': msg, 'rinfo': rinfo }) 215 | } 216 | } 217 | 218 | 219 | 220 | /** 221 | * Handles NOTIFY command 222 | * Emits `advertise-alive`, `advertise-bye` events. 223 | * 224 | * @param headers 225 | * @param _msg 226 | * @param _rinfo 227 | */ 228 | SSDP.prototype._notify = function (headers, _msg, _rinfo) { 229 | if (!headers.NTS) { 230 | this._logger('Missing NTS header: %o', headers) 231 | return 232 | } 233 | 234 | switch (headers.NTS.toLowerCase()) { 235 | // Device coming to life. 236 | case 'ssdp:alive': 237 | this.emit('advertise-alive', headers) 238 | break 239 | 240 | // Device shutting down. 241 | case 'ssdp:byebye': 242 | this.emit('advertise-bye', headers) 243 | break 244 | 245 | default: 246 | this._logger('Unhandled NOTIFY event: %o', { 'message': _msg, 'rinfo': _rinfo }) 247 | } 248 | } 249 | 250 | 251 | 252 | /** 253 | * Handles M-SEARCH command. 254 | * 255 | * @param headers 256 | * @param msg 257 | * @param rinfo 258 | */ 259 | SSDP.prototype._msearch = function (headers, msg, rinfo) { 260 | this._logger('SSDP M-SEARCH event: %o', { 'ST': headers.ST, 'address': rinfo.address, 'port': rinfo.port }) 261 | 262 | if (!headers.MAN || !headers.MX || !headers.ST) return 263 | 264 | this._respondToSearch(headers.ST, rinfo) 265 | } 266 | 267 | 268 | 269 | /** 270 | * Sends out a response to M-SEARCH commands. 271 | * 272 | * @param {String} serviceType Service type requested by a client 273 | * @param {Object} rinfo Remote client's address 274 | * @private 275 | */ 276 | SSDP.prototype._respondToSearch = function (serviceType, rinfo) { 277 | var self = this 278 | , peer = rinfo.address 279 | , port = rinfo.port 280 | , stRegex 281 | , acceptor 282 | 283 | // unwrap quoted string 284 | if (serviceType[0] == '"' && serviceType[serviceType.length - 1] == '"') { 285 | serviceType = serviceType.slice(1, -1) 286 | } 287 | 288 | if (self._allowWildcards) { 289 | stRegex = new RegExp(serviceType.replace(/\*/g, '.*') + '$') 290 | acceptor = function (usn, serviceType) { 291 | return serviceType === 'ssdp:all' || stRegex.test(usn) 292 | } 293 | } else { 294 | acceptor = function (usn, serviceType) { 295 | return serviceType === 'ssdp:all' || usn === serviceType 296 | } 297 | } 298 | 299 | Object.keys(self._usns).forEach(function (usn) { 300 | var udn = self._usns[usn] 301 | 302 | if (self._allowWildcards) { 303 | udn = udn.replace(stRegex, serviceType) 304 | } 305 | 306 | if (acceptor(usn, serviceType)) { 307 | var pkt = self._getSSDPHeader( 308 | '200 OK', 309 | { 310 | 'ST': serviceType === 'ssdp:all' ? usn : serviceType, 311 | 'USN': udn, 312 | 'LOCATION': self._location, 313 | 'CACHE-CONTROL': 'max-age=' + self._ttl, 314 | 'DATE': new Date().toUTCString(), 315 | 'SERVER': self._ssdpSig, 316 | 'EXT': '' 317 | }, 318 | true 319 | ) 320 | 321 | self._logger('Sending a 200 OK for an M-SEARCH: %o', { 'peer': peer, 'port': port }) 322 | 323 | var message = new Buffer(pkt) 324 | 325 | self._send(message, peer, port, function (err, bytes) { 326 | self._logger('Sent M-SEARCH response: %o', { 'message': pkt }) 327 | }) 328 | } 329 | }) 330 | } 331 | 332 | 333 | 334 | /** 335 | * Parses SSDP response message. 336 | * 337 | * @param msg 338 | * @param rinfo 339 | */ 340 | SSDP.prototype._parseResponse = function parseResponse(msg, rinfo) { 341 | this._logger('SSDP response: %o', { 'message': msg }) 342 | 343 | var headers = this._getHeaders(msg) 344 | , statusCode = this._getStatusCode(msg) 345 | 346 | this.emit('response', headers, statusCode, rinfo) 347 | } 348 | 349 | 350 | 351 | SSDP.prototype.addUSN = function (device) { 352 | this._usns[device] = this._udn + '::' + device 353 | } 354 | 355 | 356 | 357 | SSDP.prototype._getSSDPHeader = function (method, headers, isResponse) { 358 | var message = [] 359 | 360 | if (isResponse) { 361 | message.push('HTTP/1.1 ' + method) 362 | } else { 363 | message.push(method + ' * HTTP/1.1') 364 | } 365 | 366 | Object.keys(headers).forEach(function (header) { 367 | message.push(header + ': ' + headers[header]) 368 | }) 369 | 370 | message.push('\r\n') 371 | 372 | return message.join('\r\n') 373 | } 374 | 375 | 376 | 377 | SSDP.prototype._getMethod = function _getMethod(msg) { 378 | var lines = msg.split("\r\n") 379 | , type = lines.shift().split(' ')// command, such as "NOTIFY * HTTP/1.1" 380 | , method = type[0] 381 | 382 | return method 383 | } 384 | 385 | 386 | 387 | SSDP.prototype._getStatusCode = function _getStatusCode(msg) { 388 | var lines = msg.split("\r\n") 389 | , type = lines.shift().split(' ')// command, such as "NOTIFY * HTTP/1.1" 390 | , code = parseInt(type[1], 10) 391 | 392 | return code 393 | } 394 | 395 | 396 | 397 | SSDP.prototype._getHeaders = function _getHeaders(msg) { 398 | var lines = msg.split("\r\n") 399 | 400 | var headers = {} 401 | 402 | lines.forEach(function (line) { 403 | if (line.length) { 404 | var pairs = line.match(ssdpHeader) 405 | if (pairs) headers[pairs[1].toUpperCase()] = pairs[2] // e.g. {'HOST': 239.255.255.250:1900} 406 | } 407 | }) 408 | 409 | return headers 410 | } 411 | 412 | 413 | 414 | SSDP.prototype._send = function (message, host, port, cb) { 415 | var self = this 416 | 417 | if (typeof host === 'function') { 418 | cb = host 419 | host = this._ssdpIp 420 | port = this._ssdpPort 421 | } 422 | 423 | self.sock.send(message, 0, message.length, port, host, cb) 424 | } 425 | 426 | 427 | 428 | function getSsdpSignature() { 429 | return 'node.js/' + nodeVersion + ' UPnP/1.1 ' + moduleName + '/' + moduleVersion 430 | } 431 | 432 | 433 | 434 | module.exports = SSDP 435 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var SSDP = require('./') 2 | , util = require('util') 3 | , assert = require('assert') 4 | 5 | function SsdpServer(opts, sock) { 6 | this._subclass = 'node-ssdp:server' 7 | SSDP.call(this, opts, sock) 8 | } 9 | 10 | util.inherits(SsdpServer, SSDP) 11 | 12 | 13 | /** 14 | * Binds UDP socket to an interface/port 15 | * and starts advertising. 16 | * 17 | * @param ipAddress 18 | */ 19 | SsdpServer.prototype.start = function () { 20 | var self = this 21 | 22 | if (self._socketBound) { 23 | self._logger('Server already running.') 24 | return 25 | } 26 | 27 | self._socketBound = true 28 | 29 | this._usns[this._udn] = this._udn 30 | 31 | this._logger('Will try to bind to ' + this._unicastHost + ':' + this._ssdpPort) 32 | 33 | self._start(this._ssdpPort, this._unicastHost, this._initAdLoop.bind(this)) 34 | } 35 | 36 | 37 | /** 38 | * Binds UDP socket 39 | * 40 | * @param ipAddress 41 | * @private 42 | */ 43 | SsdpServer.prototype._initAdLoop = function () { 44 | var self = this 45 | 46 | self._logger('UDP socket bound: %o', {host: this._unicastHost, port: self._ssdpPort}) 47 | 48 | // Wake up. 49 | setTimeout(self.advertise.bind(self), 3000) 50 | 51 | self._startAdLoop() 52 | } 53 | 54 | 55 | 56 | 57 | /** 58 | * Advertise shutdown and close UDP socket. 59 | */ 60 | SsdpServer.prototype.stop = function () { 61 | if (!this.sock) { 62 | this._logger('Already stopped.') 63 | return 64 | } 65 | 66 | this.advertise(false) 67 | this.advertise(false) 68 | 69 | this._stopAdLoop() 70 | 71 | this._stop() 72 | } 73 | 74 | 75 | 76 | SsdpServer.prototype._startAdLoop = function () { 77 | assert.equal(this._adLoopInterval, null, 'Attempting to start a parallel ad loop') 78 | 79 | this._adLoopInterval = setInterval(this.advertise.bind(this), this._adInterval) 80 | } 81 | 82 | 83 | 84 | SsdpServer.prototype._stopAdLoop = function () { 85 | assert.notEqual(this._adLoopInterval, null, 'Attempting to clear a non-existing interval') 86 | 87 | clearInterval(this._adLoopInterval) 88 | this._adLoopInterval = null 89 | } 90 | 91 | 92 | 93 | /** 94 | * 95 | * @param alive 96 | */ 97 | SsdpServer.prototype.advertise = function (alive) { 98 | var self = this 99 | 100 | if (!this.sock) return 101 | if (alive === undefined) alive = true 102 | 103 | Object.keys(self._usns).forEach(function (usn) { 104 | var udn = self._usns[usn] 105 | , nts = alive ? 'ssdp:alive' : 'ssdp:byebye' // notification sub-type 106 | 107 | var heads = { 108 | 'HOST': self._ssdpServerHost, 109 | 'NT': usn, // notification type, in this case same as ST 110 | 'NTS': nts, 111 | 'USN': udn 112 | } 113 | 114 | if (alive) { 115 | heads['LOCATION'] = self._location 116 | heads['CACHE-CONTROL'] = 'max-age=1800' 117 | heads['SERVER'] = self._ssdpSig // why not include this? 118 | } 119 | 120 | self._logger('Sending an advertisement event') 121 | 122 | var message = self._getSSDPHeader('NOTIFY', heads) 123 | 124 | self._send(new Buffer(message), function (err, bytes) { 125 | self._logger('Outgoing server message: %o', {'message': message}) 126 | }) 127 | }) 128 | } 129 | 130 | module.exports = SsdpServer 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-ssdp", 3 | "description": "A node.js SSDP client and server library.", 4 | "keywords": [ 5 | "ssdp", 6 | "multicast", 7 | "media", 8 | "device", 9 | "upnp" 10 | ], 11 | "version": "2.8.2", 12 | "author": [ 13 | "Ilya Shaisultanov ", 14 | "Netbeast Staff " 15 | ], 16 | "peerDependencies": { 17 | "buffer": ">=5.0.6", 18 | "debug": ">=2.6.8", 19 | "react-native-network-info": ">=2.1.0", 20 | "react-native-udp": ">=2.0.0" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+ssh://git@github.com/netbeast/react-native-ssdp.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/netbeast/react-native-ssdp/issues" 28 | }, 29 | "main": "./index", 30 | "engines": { 31 | "node": ">=0.10.0" 32 | }, 33 | "devDependencies": { 34 | "chai": "^3.2.0", 35 | "istanbul": "~0.3.21", 36 | "mocha": "~2.3.3", 37 | "mocha-istanbul": "~0.2.0", 38 | "sinon": "~1.17.1" 39 | }, 40 | "scripts": { 41 | "test": "mocha --recursive test", 42 | "coverage": "istanbul cover node_modules/.bin/_mocha -- test/lib --recursive" 43 | }, 44 | "homepage": "https://github.com/netbeast/react-native-ssdp#readme", 45 | "directories": { 46 | "example": "example", 47 | "test": "test" 48 | }, 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon') 2 | , EE = require('events').EventEmitter 3 | 4 | beforeEach(function() { 5 | this.sinon = sinon.sandbox.create(); 6 | this.getFakeSocket = getFakeSocket.bind(this) 7 | }); 8 | 9 | afterEach(function(){ 10 | this.sinon.restore(); 11 | }); 12 | 13 | function getFakeSocket() { 14 | var s = new EE 15 | 16 | s.type = 'udp4' 17 | 18 | s.address = this.sinon.stub() 19 | s.address.returns({ 20 | address: 1, 21 | port: 2 22 | }) 23 | 24 | s.addMembership = this.sinon.stub() 25 | s.setMulticastTTL = this.sinon.stub() 26 | s.setMulticastLoopback = this.sinon.stub() 27 | 28 | s.bind = function (port, addr, cb) { 29 | cb && cb() 30 | } 31 | 32 | this.sinon.spy(s, 'bind') 33 | 34 | s.send = this.sinon.stub() 35 | s.close = this.sinon.stub() 36 | 37 | return s 38 | } -------------------------------------------------------------------------------- /test/lib/client.js: -------------------------------------------------------------------------------- 1 | require('../helper') 2 | var moduleVersion = require('../../package.json').version 3 | 4 | var expect = require('chai').expect 5 | 6 | var Client = require('../../').Client 7 | 8 | describe('Client', function () { 9 | context('when receiving a reply to M-SEARCH', function () { 10 | it('emit a parsed object', function (done) { 11 | var client = new Client(null, this.getFakeSocket()) 12 | 13 | var response = [ 14 | 'HTTP/1.1 200 OK', 15 | 'ST: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5', 16 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5', 17 | 'LOCATION: http://0.0.0.0:10000/upnp/desc.html', 18 | 'CACHE-CONTROL: max-age=1800', 19 | 'DATE: Fri, 30 May 2014 15:07:26 GMT', 20 | 'SERVER: node.js/0.10.28 UPnP/1.1 node-ssdp/' + moduleVersion, 21 | 'EXT: ' // note the space 22 | ] 23 | 24 | client.on('response', function (headers, code, rinfo) { 25 | expect(code).to.equal(200) 26 | 27 | var expected = { 28 | 'ST': 'uuid:f40c2981-7329-40b7-8b04-27f187aecfb5', 29 | 'USN': 'uuid:f40c2981-7329-40b7-8b04-27f187aecfb5', 30 | 'LOCATION': 'http://0.0.0.0:10000/upnp/desc.html', 31 | 'CACHE-CONTROL': 'max-age=1800', 32 | //'DATE': 'Fri, 30 May 2014 15:07:26 GMT', 33 | 'SERVER': 'node.js/0.10.28 UPnP/1.1 node-ssdp/' + moduleVersion, 34 | 'EXT': '' 35 | } 36 | 37 | var date = headers.DATE 38 | 39 | delete headers.DATE 40 | 41 | expect(expected).to.deep.equal(headers) 42 | expect(date).to.match(/\w+, \d+ \w+ \d+ [\d:]+ GMT/) 43 | 44 | done() 45 | }) 46 | 47 | client.start() 48 | 49 | client.sock.emit('message', Buffer(response.join('\r\n'))) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/lib/index.js: -------------------------------------------------------------------------------- 1 | require('../helper') 2 | 3 | var assert = require('assert') 4 | 5 | var SsdpBase = require('../../').Base 6 | 7 | describe('Base class', function () { 8 | context('getMethod helper', function () { 9 | it('returns correct method', function () { 10 | var ssdp = new SsdpBase 11 | 12 | var message = [ 13 | 'BLAH URI HTTP/1.1', 14 | 'SOMETHING: or other', 15 | 'AND more stuff', 16 | 'maybe not even upper case' 17 | ].join('\r\n') 18 | 19 | var method = ssdp._getMethod(message) 20 | 21 | assert.equal(method, 'BLAH') 22 | }) 23 | }) 24 | 25 | context('getHeaders helper', function () { 26 | it('returns correct headers', function () { 27 | var ssdp = new SsdpBase 28 | 29 | var message = [ 30 | 'BLAH URI HTTP/1.1', 31 | 'SOMETHING: or other', 32 | 'AND: more', 33 | 'but this one is not a real header so pass on it' 34 | ].join('\r\n') 35 | 36 | var headers = ssdp._getHeaders(message) 37 | 38 | assert.equal(Object.keys(headers).length, 2) 39 | 40 | assert.equal(headers.SOMETHING, 'or other') 41 | assert.equal(headers.AND, 'more') 42 | }) 43 | }) 44 | }) -------------------------------------------------------------------------------- /test/lib/server.js: -------------------------------------------------------------------------------- 1 | require('../helper') 2 | 3 | var assert = require('chai').assert 4 | 5 | var moduleVersion = require('../../package.json').version 6 | var Server = require('../../').Server 7 | 8 | describe('Server', function () { 9 | context('on start', function () { 10 | it('binds appropriate listeners to socket', function () { 11 | var socket = this.getFakeSocket() 12 | 13 | var server = new Server(socket) 14 | 15 | server.start() 16 | 17 | var errorHandlers = socket.listeners('error') 18 | 19 | assert.equal(errorHandlers.length, 1) 20 | assert.equal(errorHandlers[0].name, 'onSocketError') 21 | 22 | var messageHandlers = socket.listeners('message') 23 | 24 | assert.equal(messageHandlers.length, 1) 25 | assert.equal(messageHandlers[0].name, 'onSocketMessage') 26 | 27 | var listeningHandlers = socket.listeners('listening') 28 | 29 | assert.equal(listeningHandlers.length, 1) 30 | assert.equal(listeningHandlers[0].name, 'onSocketListening') 31 | }) 32 | 33 | it('does not allow double-binding on the socket', function () { 34 | var socket = this.getFakeSocket() 35 | var server = new Server(socket) 36 | 37 | this.sinon.spy(socket, 'on') 38 | 39 | server.start() 40 | server.start() 41 | server.start() 42 | 43 | assert.equal(socket.on.callCount, 3) 44 | }) 45 | 46 | it('adds multicast membership', function (done) { 47 | var socket = this.getFakeSocket() 48 | var server = new Server({ssdpIp: 'fake ip', ssdpTtl: 'never!'}, socket) 49 | 50 | server.start() 51 | 52 | socket.emit('listening') 53 | 54 | assert.equal(socket.addMembership.callCount, 1) 55 | assert(socket.addMembership.calledWith('fake ip')) 56 | 57 | assert.equal(socket.setMulticastTTL.callCount, 1) 58 | assert(socket.setMulticastTTL.calledWith('never!')) 59 | 60 | done() 61 | }) 62 | 63 | it('starts advertising every n milliseconds', function () { 64 | var clock = this.sinon.useFakeTimers() 65 | var adInterval = 500 // to avoid all other advertise timers 66 | var socket = this.getFakeSocket() 67 | var server = new Server({ssdpIp: 'fake ip', ssdpTtl: 'never!', 'adInterval': adInterval}, socket) 68 | 69 | server.addUSN('tv/video') 70 | 71 | server.start() 72 | 73 | clock.tick(500) 74 | 75 | // it's 4 because we call `advertise` immediately after bind. Lame. 76 | assert.equal(server.sock.send.callCount, 2) 77 | 78 | clock.tick(500) 79 | 80 | assert.equal(server.sock.send.callCount, 4) 81 | }) 82 | }) 83 | 84 | context('on stop', function () { 85 | it('does not allow multiple _stops', function () { 86 | var socket = this.getFakeSocket() 87 | var server = new Server(socket) 88 | 89 | server.start() 90 | 91 | assert(server.sock.bind.calledOnce) 92 | 93 | server.stop() 94 | server.stop() 95 | server.stop() 96 | 97 | assert(!server.sock) 98 | assert.equal(socket.close.callCount, 1) 99 | }) 100 | }) 101 | 102 | context('when advertising', function () { 103 | it('sends out correct alive info', function () { 104 | var clock = this.sinon.useFakeTimers() 105 | var adInterval = 500 // to avoid all other advertise timers 106 | 107 | var socket = this.getFakeSocket() 108 | var server = new Server({ 109 | ssdpIp: 'ip', 110 | ssdpTtl: 'never', 111 | unicastHost: 'unicast', 112 | location: 'location header', 113 | adInterval: adInterval, 114 | ssdpSig: 'signature', 115 | ttl: 'ttl', 116 | description: 'desc', 117 | udn: 'device name' 118 | }, socket) 119 | 120 | var _advertise = server.advertise 121 | 122 | this.sinon.stub(server, 'advertise', function (alive) { 123 | if (alive === false) return 124 | _advertise.call(server) 125 | }) 126 | 127 | server.addUSN('tv/video') 128 | 129 | server.start() 130 | 131 | clock.tick(500) 132 | 133 | // server.sock.send should've been called 2 times with 2 unique args 134 | assert.equal(server.sock.send.callCount, 2) 135 | 136 | // argument order is: 137 | // message, _, message.length, ssdp port, ssdp host 138 | var args1 = server.sock.send.getCall(0).args 139 | 140 | var method1 = server._getMethod(args1[0].toString()) 141 | assert(method1, 'NOTIFY') 142 | 143 | var headers1 = server._getHeaders(args1[0].toString()) 144 | assert.equal(headers1.HOST, 'ip:1900') 145 | assert.equal(headers1.NT, 'tv/video') 146 | assert.equal(headers1.NTS, 'ssdp:alive') 147 | assert.equal(headers1.USN, 'device name::tv/video') 148 | assert.equal(headers1.LOCATION, 'location header') 149 | assert.equal(headers1['CACHE-CONTROL'], 'max-age=1800') 150 | assert.equal(headers1.SERVER, 'signature') 151 | 152 | var port1 = args1[3] 153 | assert.equal(port1, 1900) 154 | 155 | var host1 = args1[4] 156 | assert.equal(host1, 'ip') 157 | 158 | var args2 = server.sock.send.getCall(1).args 159 | 160 | var method2 = server._getMethod(args2[0].toString()) 161 | assert(method2, 'NOTIFY') 162 | 163 | var headers2 = server._getHeaders(args2[0].toString()) 164 | assert.equal(headers2.HOST, 'ip:1900') 165 | assert.equal(headers2.NT, 'device name') 166 | assert.equal(headers2.NTS, 'ssdp:alive') 167 | assert.equal(headers2.USN, 'device name') 168 | assert.equal(headers2.LOCATION, 'location header') 169 | assert.equal(headers2['CACHE-CONTROL'], 'max-age=1800') 170 | assert.equal(headers2.SERVER, 'signature') 171 | 172 | var port2 = args2[3] 173 | assert.equal(port2, 1900) 174 | 175 | var host2 = args2[4] 176 | assert.equal(host2, 'ip') 177 | }) 178 | 179 | it('sends out correct byebye info', function () { 180 | var adInterval = 500 // to avoid all other advertise timers 181 | 182 | var socket = this.getFakeSocket() 183 | var server = new Server({ 184 | ssdpIp: 'ip', 185 | ssdpTtl: 'never', 186 | adInterval: adInterval, 187 | ssdpSig: 'signature', 188 | unicastHost: 'unicast', 189 | location: 'location header', 190 | ttl: 'ttl', 191 | description: 'desc', 192 | udn: 'device name' 193 | }, socket) 194 | 195 | // avoid calling server.start 196 | 197 | server._adLoopInterval = 1 198 | 199 | server.addUSN('tv/video') 200 | 201 | server.stop() 202 | 203 | // server.sock.send should've been called 2 times with 2 unique args 204 | assert.equal(socket.send.callCount, 2) 205 | 206 | // argument order is: 207 | // message, _, message.length, ssdp port, ssdp host 208 | var args1 = socket.send.getCall(0).args 209 | 210 | var method1 = server._getMethod(args1[0].toString()) 211 | assert(method1, 'NOTIFY') 212 | 213 | var headers1 = server._getHeaders(args1[0].toString()) 214 | assert.equal(headers1.HOST, 'ip:1900') 215 | assert.equal(headers1.NT, 'tv/video') 216 | assert.equal(headers1.NTS, 'ssdp:byebye') 217 | assert.equal(headers1.USN, 'device name::tv/video') 218 | assert.equal(headers1.LOCATION, undefined) 219 | assert.equal(headers1['CACHE-CONTROL'], undefined) 220 | assert.equal(headers1.SERVER, undefined) 221 | 222 | var port1 = args1[3] 223 | assert.equal(port1, 1900) 224 | 225 | var host1 = args1[4] 226 | assert.equal(host1, 'ip') 227 | 228 | }) 229 | }) 230 | 231 | context('when receiving a message with unknown command', function () { 232 | //FIXME Ctrl-C, Ctrl-V! 233 | var UNKNOWN_CMD = [ 234 | 'LOLWUT * HTTP/1.1', 235 | 'HOST: 239.255.255.250:1900', 236 | 'NT: upnp:rootdevice', 237 | 'NTS: ssdp:alive', 238 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5::upnp:rootdevice', 239 | 'LOCATION: http://192.168.1.1:10293/upnp/desc.html', 240 | 'CACHE-CONTROL: max-age=1800', 241 | 'SERVER: node.js/0.10.28 UPnP/1.1 node-ssdp/' + moduleVersion 242 | ].join('\r\n') 243 | 244 | it('server emits nothing but logs it', function (done) { 245 | var server = new Server(this.getFakeSocket()) 246 | 247 | server.start() 248 | 249 | this.sinon.spy(server, 'emit') 250 | 251 | server._logger = function (message, data) { 252 | if (message.indexOf('Unhandled command') === -1) return 253 | 254 | assert.equal(data.rinfo.address, 1) 255 | assert.equal(data.rinfo.port, 2) 256 | 257 | assert(server.emit.notCalled) 258 | 259 | done() 260 | } 261 | 262 | server.sock.emit('message', UNKNOWN_CMD, {address: 1, port: 2}) 263 | }) 264 | }) 265 | 266 | context('when receiving a NOTIFY message', function () { 267 | //FIXME Ctrl-C, Ctrl-V! 268 | var NOTIFY_ALIVE = [ 269 | 'NOTIFY * HTTP/1.1', 270 | 'HOST: 239.255.255.250:1900', 271 | 'NT: upnp:rootdevice', 272 | 'NTS: ssdp:alive', 273 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5::upnp:rootdevice', 274 | 'LOCATION: http://192.168.1.1:10293/upnp/desc.html', 275 | 'CACHE-CONTROL: max-age=1800', 276 | 'SERVER: node.js/0.10.28 UPnP/1.1 node-ssdp/' + moduleVersion 277 | ].join('\r\n') 278 | 279 | var NOTIFY_BYE = [ 280 | 'NOTIFY * HTTP/1.1', 281 | 'HOST: 239.255.255.250:1900', 282 | 'NT: upnp:rootdevice', 283 | 'NTS: ssdp:byebye', 284 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5::upnp:rootdevice' 285 | ].join('\r\n') 286 | 287 | var NOTIFY_WTF = [ 288 | 'NOTIFY * HTTP/1.1', 289 | 'HOST: 239.255.255.250:1900', 290 | 'NT: upnp:rootdevice', 291 | 'NTS: WAT', 292 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5::upnp:rootdevice' 293 | ].join('\r\n') 294 | 295 | it('with ssdp:alive server emits `advertise-alive` with data', function (done) { 296 | var server = new Server(this.getFakeSocket()) 297 | 298 | server.on('advertise-alive', function (headers) { 299 | ['HOST', 'NT', 'NTS', 'USN', 'LOCATION', 'CACHE-CONTROL', 'SERVER'].forEach(function (header) { 300 | assert(headers[header]) 301 | }) 302 | 303 | done() 304 | }) 305 | 306 | server.start() 307 | 308 | server.sock.emit('message', NOTIFY_ALIVE, {address: 1, port: 2}) 309 | }) 310 | 311 | it('with ssdp:bye server emits `advertise-bye` with data', function (done) { 312 | var server = new Server(this.getFakeSocket()) 313 | 314 | server.on('advertise-bye', function (headers) { 315 | ['HOST', 'NT', 'NTS', 'USN'].forEach(function (header) { 316 | assert(headers[header]) 317 | }) 318 | 319 | done() 320 | }) 321 | 322 | server.start() 323 | 324 | server.sock.emit('message', NOTIFY_BYE, {address: 1, port: 2}) 325 | }) 326 | 327 | it('with unknown NTS server emits nothing but logs it', function (done) { 328 | var server = new Server(this.getFakeSocket()) 329 | 330 | server.start() 331 | 332 | this.sinon.spy(server, 'emit') 333 | 334 | server._logger = function (message, data) { 335 | if (message.indexOf('Unhandled NOTIFY event') === -1) return 336 | 337 | assert.equal(data.rinfo.address, 1) 338 | assert.equal(data.rinfo.port, 2) 339 | 340 | assert(server.emit.notCalled) 341 | 342 | done() 343 | } 344 | 345 | server.sock.emit('message', NOTIFY_WTF, {address: 1, port: 2}) 346 | }) 347 | }) 348 | 349 | context('when receiving an M-SEARCH message', function () { 350 | it('with unknown service type it\'s ignored', function (done) { 351 | var server = new Server(this.getFakeSocket()) 352 | 353 | server.advertise = this.sinon.stub() // otherwise it'll call `send` 354 | 355 | server.start() 356 | 357 | this.sinon.spy(server, '_respondToSearch') 358 | 359 | var MS_UNKNOWN = [ 360 | 'M-SEARCH * HTTP/1.1', 361 | 'HOST: 239.255.255.250:1900', 362 | 'ST: toaster', 363 | 'MAN: "ssdp:discover"', 364 | 'MX: 3' 365 | ].join('\r\n') 366 | 367 | server.sock.emit('message', MS_UNKNOWN, {address: 1, port: 2}) 368 | 369 | assert(server._respondToSearch.calledOnce) 370 | assert(server.sock.send.notCalled) 371 | 372 | done() 373 | }) 374 | 375 | it('with ssdp:all service type it replies with a unicast 200 OK', function (done) { 376 | var server = new Server(this.getFakeSocket()) 377 | 378 | server.advertise = this.sinon.stub() // otherwise it'll call `send` 379 | 380 | server.start() 381 | 382 | this.sinon.spy(server, '_respondToSearch') 383 | 384 | var MS_ALL = [ 385 | 'M-SEARCH * HTTP/1.1', 386 | 'HOST: 239.255.255.250:1900', 387 | 'ST: ssdp:all', 388 | 'MAN: "ssdp:discover"', 389 | 'MX: 3' 390 | ].join('\r\n') 391 | 392 | server.sock.emit('message', MS_ALL, {address: 1, port: 2}) 393 | 394 | assert(server._respondToSearch.calledOnce) 395 | assert(server.sock.send.calledOnce) 396 | 397 | var args = server.sock.send.getCall(0).args 398 | , message = args[0] 399 | , port = args[3] 400 | , ip = args[4] 401 | 402 | assert(Buffer.isBuffer(message)) 403 | assert.equal(port, 2) 404 | assert.equal(ip, 1) 405 | 406 | var expectedMessage = [ 407 | 'HTTP/1.1 200 OK', 408 | 'ST: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5', 409 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5', 410 | 'LOCATION: http://' + require('ip').address() + ':10293/upnp/desc.html', 411 | 'CACHE-CONTROL: max-age=1800', 412 | //'DATE: Fri, 30 May 2014 15:07:26 GMT', we'll test for this separately 413 | 'SERVER: node.js/' + process.versions.node + ' UPnP/1.1 node-ssdp/' + moduleVersion, 414 | 'EXT: ' // note the space 415 | ] 416 | 417 | message = message.toString().split('\r\n') 418 | 419 | var filteredMessage = message.filter(function (header) { 420 | return !/^DATE/.test(header) && header !== '' 421 | }) 422 | 423 | assert.deepEqual(filteredMessage, expectedMessage) 424 | 425 | var dateHeader = message.filter(function (header) { 426 | return /^DATE/.test(header) 427 | })[0] 428 | 429 | // should look like UTC string 430 | assert(/\w+, \d+ \w+ \d+ [\d:]+ GMT/.test(dateHeader)) 431 | 432 | done() 433 | }) 434 | 435 | it('with matching wildcard it replies with a unicast 200 OK', function (done) { 436 | var server = new Server({ 437 | allowWildcards: true 438 | }, this.getFakeSocket()) 439 | server.addUSN('urn:Manufacturer:device:controllee:1') 440 | 441 | server.advertise = this.sinon.stub() // otherwise it'll call `send` 442 | 443 | server.start() 444 | 445 | this.sinon.spy(server, '_respondToSearch') 446 | 447 | var MS_ALL = [ 448 | 'M-SEARCH * HTTP/1.1', 449 | 'HOST: 239.255.255.250:1900', 450 | 'ST: urn:Manufacturer:device:*', 451 | 'MAN: "ssdp:discover"', 452 | 'MX: 3' 453 | ].join('\r\n') 454 | 455 | server.sock.emit('message', MS_ALL, {address: 1, port: 2}) 456 | 457 | assert(server._respondToSearch.calledOnce) 458 | assert(server.sock.send.calledOnce) 459 | 460 | var args = server.sock.send.getCall(0).args 461 | , message = args[0] 462 | , port = args[3] 463 | , ip = args[4] 464 | 465 | assert(Buffer.isBuffer(message)) 466 | assert.equal(port, 2) 467 | assert.equal(ip, 1) 468 | 469 | var expectedMessage = [ 470 | 'HTTP/1.1 200 OK', 471 | 'ST: urn:Manufacturer:device:*', 472 | 'USN: uuid:f40c2981-7329-40b7-8b04-27f187aecfb5::urn:Manufacturer:device:*', 473 | 'LOCATION: http://' + require('ip').address() + ':10293/upnp/desc.html', 474 | 'CACHE-CONTROL: max-age=1800', 475 | //'DATE: Fri, 30 May 2014 15:07:26 GMT', we'll test for this separately 476 | 'SERVER: node.js/' + process.versions.node + ' UPnP/1.1 node-ssdp/' + moduleVersion, 477 | 'EXT: ' // note the space 478 | ] 479 | 480 | message = message.toString().split('\r\n') 481 | 482 | var filteredMessage = message.filter(function (header) { 483 | return !/^DATE/.test(header) && header !== '' 484 | }) 485 | 486 | assert.deepEqual(filteredMessage, expectedMessage) 487 | 488 | var dateHeader = message.filter(function (header) { 489 | return /^DATE/.test(header) 490 | })[0] 491 | 492 | // should look like UTC string 493 | assert(/\w+, \d+ \w+ \d+ [\d:]+ GMT/.test(dateHeader)) 494 | 495 | done() 496 | }) 497 | }) 498 | }) 499 | --------------------------------------------------------------------------------