Permalink
Browse files

Implement ControlPoint class with basic SSDP discovery.

Signed-off-by: Pierre Buyle <pierre@buyle.org>
  • Loading branch information...
1 parent cacfad5 commit 48c1976f5eb1a8cb8d9d28b43c9bb8c6f91fb350 @pbuyle pbuyle committed Sep 4, 2011
Showing with 179 additions and 107 deletions.
  1. +4 −0 .gitignore
  2. +12 −46 README.md
  3. +14 −0 examples/spy.js
  4. +148 −60 lib/upnp.js
  5. +1 −1 package.json
View
@@ -0,0 +1,4 @@
+/node_modules
+/.buildpath
+/.project
+/.settings
View
@@ -4,13 +4,6 @@ node-upnp-client
A module for NodeJS written in JavaScript to interface with UPnP compliant devices.
-The entire range of UPnP devices aims to be supported through contributors.
-Currently implemented specifications are:
-
- - InternetGatewayDevice:1
- - WANDevice:1
- - WANIPConnection:1
-
Usage
-----
@@ -25,49 +18,22 @@ UPnP-related:
var upnp = require("upnp");
var controlPoint = new upnp.ControlPoint();
- controlPoint.on("deviceAdded", function(err, device) {
- console.log(device.deviceType);
+ controlPoint.on("DeviceAvailable", function(device) {
+ console.log(device.nt);
//-> "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
console.log(device.location);
//-> "http://192.168.0.1/root.sxml"
});
-
-#### Description
-
-Once a device is "discovered", there's still not very much known about the device other
-than it's _root_ device type. The `loadDescription` function needs to be called on a
-"device" object to determine which services, child devices, events, etc. the device
-implements. The `prototype` of the device object gets extended to reflect the parsed
-description of the device:
-
- device.loadDescription(function(err) {
- console.log(device);
- // 'device' is now populated with a lot more properties
- });
-
-#### Control
-
-Invoking the services that a device exposes is probably your primary concern with
-interacting with a UPnP device. `GetExternalIPAddress` is an example of an _action_
-exposed from a _WANIPConnection_ service:
-
- device.GetExternalIPAddress(function(err, ip) {
- console.log(ip);
- //-> "1.1.1.1"
- });
-
-#### Event Notification
-
-Individual device "service"s often emit their own events when certain properties
-of the device change. Use the `notification` event of Device or Service
-instances to invoke a callback whenever a notification is emitter from the device:
-
- device.on("notification", function(properties) {
- for (var i in properties) {
- console.log(i + ": " + properties[i]);
- }
- });
-
+
+ controlPoint.on("DeviceFound", function(device) {
+ console.log(device.st);
+ //-> "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
+ console.log(device.location);
+ //-> "http://192.168.0.1/root.sxml"
+ }
+
+ controlPoint.search('urn:schemas-upnp-org:device:InternetGatewayDevice:1');
+
[UPnP]: http://upnp.org/
[NodeJS]: http://nodejs.org
View
@@ -0,0 +1,14 @@
+var upnp = require('../lib/upnp');
+var util = require('util');
+var log = function(event) {
+ return function(device) {
+ console.log('UPNP Event %s: %s, %s', event, device.nt || device.st, device.usn);
+ };
+};
+
+cp = new upnp.ControlPoint();
+cp.on('DeviceAvailable', log('DeviceAvailable'));
+cp.on('DeviceUpdated', log('DeviceUpdated'));
+cp.on('DeviceUnavailable', log('DeviceUnavailable'));
+cp.on('DeviceFound', log('DeviceFound'));
+cp.search();
View
@@ -1,71 +1,178 @@
var url = require("url");
var http = require("http");
var dgram = require("dgram");
+var util = require("util");
+var events = require("events");
-// some const strings - dont change
+// SSDP
const SSDP_PORT = 1900;
const BROADCAST_ADDR = "239.255.255.250";
-const ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";
-const req = "M-SEARCH * HTTP/1.1\r\nHost:"+BROADCAST_ADDR+":"+SSDP_PORT+"\r\nST:"+ST+"\r\nMan:\"ssdp:discover\"\r\nMX:3\r\n\r\n";
+const SSDP_MSEARCH = "M-SEARCH * HTTP/1.1\r\nHost:"+BROADCAST_ADDR+":"+SSDP_PORT+"\r\nST:%st\r\nMan:\"ssdp:discover\"\r\nMX:3\r\n\r\n";
+const SSDP_ALIVE = 'ssdp:alive';
+const SSDP_BYEBYE = 'ssdp:byebye';
+const SSDP_UPDATE = 'ssdp:update';
+const SSDP_ALL = 'ssdp:all';
+
+// Map SSDP notification sub type to emitted events
+const UPNP_NTS_EVENTS = {
+ 'ssdp:alive': 'DeviceAvailable',
+ 'ssdp:byebye': 'DeviceUnavailable',
+ 'ssdp:update': 'DeviceUpdate'
+};
+
+var debug;
+if (process.env.NODE_DEBUG && /upnp/.test(process.env.NODE_DEBUG)) {
+ debug = function(x) { console.error('UPNP: %s', x); };
+
+} else {
+ debug = function() { };
+}
+
+function ControlPoint() {
+ events.EventEmitter.call(this);
+ this.server = dgram.createSocket('udp4');
+ this.server.addMembership(BROADCAST_ADDR);
+ var self = this;
+ this.server.on('message', function(msg, rinfo) {self.onRequestMessage(msg, rinfo);});
+ this._initParsers();
+ this.server.bind(SSDP_PORT);
+}
+util.inherits(ControlPoint, events.EventEmitter);
+exports.ControlPoint = ControlPoint;
+
+/**
+ * Message handler for HTTPU request.
+ */
+ControlPoint.prototype.onRequestMessage = function(msg, rinfo) {
+ var ret = this.requestParser.execute(msg, 0, msg.length);
+ if (!(ret instanceof Error)) {
+ var req = this.requestParser.incoming;
+ switch (req.method) {
+ case 'NOTIFY':
+ debug('NOTIFY ' + req.headers.nts + ' NT=' + req.headers.nt + ' USN=' + req.headers.usn);
+ var event = UPNP_NTS_EVENTS[req.headers.nts];
+ if (event) {
+ this.emit(event, req.headers);
+ }
+ break;
+ };
+ }
+};
+
+/**
+ * Message handler for HTTPU response.
+ */
+ControlPoint.prototype.onResponseMessage = function(msg, rinfo){
+ var ret = this.responseParser.execute(msg, 0, msg.length);
+ if (!(ret instanceof Error)) {
+ var res = this.responseParser.incoming;
+ if (res.statusCode == 200) {
+ debug('RESPONSE ST=' + res.headers.st + ' USN=' + res.headers.usn);
+ this.emit('DeviceFound', res.headers);
+ }
+ }
+}
+
+/**
+ * Initialize HTTPU response and request parsers.
+ */
+ControlPoint.prototype._initParsers = function() {
+ var self = this;
+ if (!self.requestParser) {
+ self.requestParser = http.parsers.alloc();
+ self.requestParser.reinitialize('request');
+ self.requestParser.onIncoming = function(req) {
+
+ };
+ }
+ if (!self.responseParser) {
+ self.responseParser = http.parsers.alloc();
+ self.responseParser.reinitialize('response');
+ self.responseParser.onIncoming = function(res) {
+
+ };
+ }
+};
+
+/**
+ * Send an SSDP search request.
+ *
+ * Listen for the <code>DeviceFound</code> event to catch found devices or services.
+ *
+ * @param String st
+ * The search target for the request (optional, defaults to "ssdp:all").
+ */
+ControlPoint.prototype.search = function(st) {
+ if (typeof st !== 'string') {
+ st = SSDP_ALL;
+ }
+ var message = new Buffer(SSDP_MSEARCH.replace('%st', st), "ascii");
+ var client = dgram.createSocket("udp4");
+ client.bind(); // So that we get a port so we can listen before sending
+ // Set a server to listen for responses
+ var server = dgram.createSocket('udp4');
+ var self = this;
+ server.on('message', function(msg, rinfo) {self.onResponseMessage(msg, rinfo);});
+ server.bind(client.address().port);
+
+ // Broadcast request
+ client.send(message, 0, message.length, SSDP_PORT, BROADCAST_ADDR);
+ debug('REQUEST SEARCH ' + st);
+ client.close();
+
+ // MX is set to 3, wait for 1 additional sec. before closing the server
+ setTimeout(function(){
+ server.close();
+ }, 4000);
+}
+
+/**
+ * Terminates this ControlPoint.
+ */
+ControlPoint.prototype.close = function() {
+ this.server.close();
+ http.parsers.free(this.requestParser);
+ http.parsers.free(this.responseParser);
+}
+
+/* TODO Move these stuff to a separated module/project */
+
+//some const strings - dont change
+const GW_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";
const WANIP = "urn:schemas-upnp-org:service:WANIPConnection:1";
const OK = "HTTP/1.1 200 OK";
const SOAP_ENV_PRE = "<?xml version=\"1.0\"?>\n<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>";
const SOAP_ENV_POST = "</s:Body></s:Envelope>";
function searchGateway(timeout, callback) {
-
- var self = this;
- var reqbuf = new Buffer(req, "ascii");
- var socket = new dgram.Socket("udp4");
var clients = {};
var t;
if (timeout) {
- t = setTimeout(function() {
- onerror(new Error("searchGateway() timed out"));
+ t = setTimeout(function() {
+ callback(new Error("searchGateway() timed out"));
}, timeout);
}
- var onlistening = function() {
-
- process.binding('net').setBroadcast(socket.fd, true);
- // send a few packets just in case.
- for (var i = 0; i < 4; i++)
- socket.send(reqbuf, 0, reqbuf.length, SSDP_PORT, BROADCAST_ADDR);
-
- }
-
- var onmessage = function(message, rinfo) {
-
- message = message.toString();
-
- if (message.substr(0, OK.length) !== OK ||
- message.indexOf(ST) === -1 ||
- message.toLowerCase().indexOf("location:") === -1) return;
-
-
- console.error(message);
-
-
- var l = url.parse(message.match(/location:(.+?)\r\n/i)[1].trim());
- l.port = l.port || (l.protocol == "https:" ? 443:80);
+ var cp = new ControlPoint();
+ cp.on('DeviceFound', function(headers) {
+ var l = url.parse(headers.location);
+ l.port = l.port || (l.protocol == "https:" ? 443 : 80);
+ // Early return if this location is already processed
if (clients[l.href]) return;
-
-
- //console.error(l);
-
-
+
+ // Retrieve device/service description
var client = clients[l.href] = http.createClient(l.port, l.hostname);
var request = client.request("GET", l.pathname, {
"Host": l.hostname
});
request.addListener('response', function (response) {
if (response.statusCode !== 200) {
- socket.emit(new Error("Unexpected response status code: " + response.statusCode));
+ callback(new Error("Unexpected response status code: " + response.statusCode));
}
var resbuf = "";
response.setEncoding("utf8");
- response.addListener('data', function (chunk) { resbuf += chunk });
+ response.addListener('data', function (chunk) { resbuf += chunk;});
response.addListener("end", function() {
resbuf = resbuf.substr(resbuf.indexOf(WANIP) + WANIP.length);
var ipurl = resbuf.match(/<controlURL>(.+?)<\/controlURL>/i)[1].trim()
@@ -78,28 +185,9 @@ function searchGateway(timeout, callback) {
});
});
request.end();
- }
-
- var onerror = function(err) {
- socket.close() ;
- clearTimeout(t);
- callback(err);
- }
-
- var onclose = function() {
- socket.removeListener("listening", onlistening);
- socket.removeListener("message", onmessage);
- socket.removeListener("close", onclose);
- socket.removeListener("error", onerror);
- }
-
- socket.addListener("listening", onlistening);
- socket.addListener("message", onmessage);
- socket.addListener("close", onclose);
- socket.addListener("error", onerror);
-
- socket.bind(SSDP_PORT);
+ });
+ cp.search(GW_ST);
}
exports.searchGateway = searchGateway;
View
@@ -2,7 +2,7 @@
"name": "upnp-client",
"description": "A Client Library to interface with UPnP compliant devices.",
"version": "0.0.0",
- "author": "Nathan Rajlich <nathan@tootallnate.net>",
+ "author": "Nathan Rajlich <nathan@tootallnate.net>, Pierre Buyle <pierre@buyle.org>",
"dependencies": {
"step": ">= 0.0.3"
},

0 comments on commit 48c1976

Please sign in to comment.