@homebridge/dbus-native 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ const Buffer = require('safe-buffer').Buffer;
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const constants = require('./constants');
7
+ const readLine = require('./readline');
8
+
9
+ function sha1(input) {
10
+ var shasum = crypto.createHash('sha1');
11
+ shasum.update(input);
12
+ return shasum.digest('hex');
13
+ }
14
+
15
+ function getUserHome() {
16
+ return process.env[process.platform.match(/$win/) ? 'USERPROFILE' : 'HOME'];
17
+ }
18
+
19
+ function getCookie(context, id, cb) {
20
+ // http://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha
21
+ var dirname = path.join(getUserHome(), '.dbus-keyrings');
22
+ // > There is a default context, "org_freedesktop_general" that's used by servers that do not specify otherwise.
23
+ if (context.length === 0) context = 'org_freedesktop_general';
24
+
25
+ var filename = path.join(dirname, context);
26
+ // check it's not writable by others and readable by user
27
+ fs.stat(dirname, function(err, stat) {
28
+ if (err) return cb(err);
29
+ if (stat.mode & 0o22)
30
+ return cb(
31
+ new Error(
32
+ 'User keyrings directory is writeable by other users. Aborting authentication'
33
+ )
34
+ );
35
+ if (process.hasOwnProperty('getuid') && stat.uid !== process.getuid())
36
+ return cb(
37
+ new Error(
38
+ 'Keyrings directory is not owned by the current user. Aborting authentication!'
39
+ )
40
+ );
41
+ fs.readFile(filename, 'ascii', function(err, keyrings) {
42
+ if (err) return cb(err);
43
+ var lines = keyrings.split('\n');
44
+ for (var l = 0; l < lines.length; ++l) {
45
+ var data = lines[l].split(' ');
46
+ if (id === data[0]) return cb(null, data[2]);
47
+ }
48
+ return cb(new Error('cookie not found'));
49
+ });
50
+ });
51
+ }
52
+
53
+ function hexlify(input) {
54
+ return Buffer.from(input.toString(), 'ascii').toString('hex');
55
+ }
56
+
57
+ module.exports = function auth(stream, opts, cb) {
58
+ // filter used to make a copy so we don't accidently change opts data
59
+ var authMethods;
60
+ if (opts.authMethods) {
61
+ authMethods = opts.authMethods;
62
+ } else {
63
+ authMethods = constants.defaultAuthMethods;
64
+ }
65
+ stream.write('\0');
66
+ tryAuth(stream, authMethods.slice(), cb);
67
+ };
68
+
69
+ function tryAuth(stream, methods, cb) {
70
+ if (methods.length === 0) {
71
+ return cb(new Error('No authentication methods left to try'));
72
+ }
73
+
74
+ var authMethod = methods.shift();
75
+ var uid = process.hasOwnProperty('getuid') ? process.getuid() : 0;
76
+ var id = hexlify(uid);
77
+
78
+ function beginOrNextAuth() {
79
+ readLine(stream, function(line) {
80
+ var ok = line.toString('ascii').match(/^([A-Za-z]+) (.*)/);
81
+ if (ok && ok[1] === 'OK') {
82
+ stream.write('BEGIN\r\n');
83
+ return cb(null, ok[2]); // ok[2] = guid. Do we need it?
84
+ } else {
85
+ // TODO: parse error!
86
+ if (!methods.empty) {
87
+ tryAuth(stream, methods, cb);
88
+ } else {
89
+ return cb(line);
90
+ }
91
+ }
92
+ });
93
+ }
94
+
95
+ switch (authMethod) {
96
+ case 'EXTERNAL':
97
+ stream.write(`AUTH ${authMethod} ${id}\r\n`);
98
+ beginOrNextAuth();
99
+ break;
100
+ case 'DBUS_COOKIE_SHA1':
101
+ stream.write(`AUTH ${authMethod} ${id}\r\n`);
102
+ readLine(stream, function(line) {
103
+ var data = Buffer.from(
104
+ line
105
+ .toString()
106
+ .split(' ')[1]
107
+ .trim(),
108
+ 'hex'
109
+ )
110
+ .toString()
111
+ .split(' ');
112
+ var cookieContext = data[0];
113
+ var cookieId = data[1];
114
+ var serverChallenge = data[2];
115
+ // any random 16 bytes should work, sha1(rnd) to make it simplier
116
+ var clientChallenge = crypto.randomBytes(16).toString('hex');
117
+ getCookie(cookieContext, cookieId, function(err, cookie) {
118
+ if (err) return cb(err);
119
+ var response = sha1(
120
+ [serverChallenge, clientChallenge, cookie].join(':')
121
+ );
122
+ var reply = hexlify(clientChallenge + response);
123
+ stream.write(`DATA ${reply}\r\n`);
124
+ beginOrNextAuth();
125
+ });
126
+ });
127
+ break;
128
+ case 'ANONYMOUS':
129
+ stream.write('AUTH ANONYMOUS \r\n');
130
+ beginOrNextAuth();
131
+ break;
132
+ default:
133
+ console.error(`Unsupported auth method: ${authMethod}`);
134
+ beginOrNextAuth();
135
+ break;
136
+ }
137
+ }
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "type": "a",
4
+ "child": [
5
+ {
6
+ "type": "(",
7
+ "child": [
8
+ {
9
+ "type": "y",
10
+ "child": []
11
+ },
12
+ {
13
+ "type": "v",
14
+ "child": []
15
+ }
16
+ ]
17
+ }
18
+ ]
19
+ }
20
+ ]
@@ -0,0 +1,16 @@
1
+ const Buffer = require('safe-buffer').Buffer;
2
+
3
+ // Pre-serialised hello message. serial = 1
4
+ module.exports = function() {
5
+ return Buffer.from(
6
+ `6c01 0001 0000 0000 0100 0000 6d00 0000
7
+ 0101 6f00 1500 0000 2f6f 7267 2f66 7265
8
+ 6564 6573 6b74 6f70 2f44 4275 7300 0000
9
+ 0301 7300 0500 0000 4865 6c6c 6f00 0000
10
+ 0201 7300 1400 0000 6f72 672e 6672 6565
11
+ 6465 736b 746f 702e 4442 7573 0000 0000
12
+ 0601 7300 1400 0000 6f72 672e 6672 6565
13
+ 6465 736b 746f 702e 4442 7573 0000 0000`.replace(/ |\n/g, ''),
14
+ 'hex'
15
+ );
16
+ };
@@ -0,0 +1,201 @@
1
+ const xml2js = require('xml2js');
2
+
3
+ module.exports.introspectBus = function(obj, callback) {
4
+ var bus = obj.service.bus;
5
+ bus.invoke(
6
+ {
7
+ destination: obj.service.name,
8
+ path: obj.name,
9
+ interface: 'org.freedesktop.DBus.Introspectable',
10
+ member: 'Introspect'
11
+ },
12
+ function(err, xml) { module.exports.processXML(err, xml, obj, callback); }
13
+ );
14
+ };
15
+
16
+ module.exports.processXML = function(err, xml, obj, callback) {
17
+ if (err) return callback(err);
18
+ var parser = new xml2js.Parser();
19
+ parser.parseString(xml, function(err, result) {
20
+ if (err) return callback(err);
21
+ if (!result.node) throw new Error('No root XML node');
22
+ result = result.node; // unwrap the root node
23
+ // If no interface, try first sub node?
24
+ if (!result.interface) {
25
+ if (result.node && result.node.length > 0 && result.node[0]['$']) {
26
+ var subObj = Object.assign(obj, {});
27
+ if (subObj.name.slice(-1) !== '/') subObj.name += '/';
28
+ subObj.name += result.node[0]['$'].name;
29
+ return module.exports.introspectBus(subObj, callback);
30
+ }
31
+ return callback(new Error('No such interface found'));
32
+ }
33
+ var proxy = {};
34
+ var nodes = [];
35
+ var ifaceName, method, property, iface, arg, signature, currentIface;
36
+ var ifaces = result['interface'];
37
+ var xmlnodes = result['node'] || [];
38
+
39
+ for (var n = 1; n < xmlnodes.length; ++n) {
40
+ // Start at 1 because we want to skip the root node
41
+ nodes.push(xmlnodes[n]['$']['name']);
42
+ }
43
+
44
+ for (var i = 0; i < ifaces.length; ++i) {
45
+ iface = ifaces[i];
46
+ ifaceName = iface['$'].name;
47
+ currentIface = proxy[ifaceName] = new DBusInterface(obj, ifaceName);
48
+
49
+ for (var m = 0; iface.method && m < iface.method.length; ++m) {
50
+ method = iface.method[m];
51
+ signature = '';
52
+ var methodName = method['$'].name;
53
+ for (var a = 0; method.arg && a < method.arg.length; ++a) {
54
+ arg = method.arg[a]['$'];
55
+ if (arg.direction === 'in') signature += arg.type;
56
+ }
57
+ // add method
58
+ currentIface.$createMethod(methodName, signature);
59
+ }
60
+ for (var p = 0; iface.property && p < iface.property.length; ++p) {
61
+ property = iface.property[p];
62
+ currentIface.$createProp(property['$'].name, property['$'].type, property['$'].access)
63
+ }
64
+ // TODO: introspect signals
65
+ }
66
+ callback(null, proxy, nodes);
67
+ });
68
+ }
69
+
70
+
71
+ function DBusInterface(parent_obj, ifname)
72
+ {
73
+ // Since methods and props presently get added directly to the object, to avoid collision with existing names we must use $ naming convention as $ is invalid for dbus member names
74
+ // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
75
+ this.$parent = parent_obj; // parent DbusObject
76
+ this.$name = ifname; // string interface name
77
+ this.$methods = {}; // dictionary of methods (exposed for test), should we just store signature or use object to store more info?
78
+ //this.$signals = {};
79
+ this.$properties = {};
80
+ this.$callbacks = [];
81
+ this.$sigHandlers = [];
82
+ }
83
+ DBusInterface.prototype.$getSigHandler = function(callback) {
84
+ var index;
85
+ if ((index = this.$callbacks.indexOf(callback)) === -1) {
86
+ index = this.$callbacks.push(callback) - 1;
87
+ this.$sigHandlers[index] = function(messageBody) {
88
+ callback.apply(null, messageBody);
89
+ };
90
+ }
91
+ return this.$sigHandlers[index];
92
+ }
93
+ DBusInterface.prototype.addListener = DBusInterface.prototype.on = function(signame, callback) {
94
+ // http://dbus.freedesktop.org/doc/api/html/group__DBusBus.html#ga4eb6401ba014da3dbe3dc4e2a8e5b3ef
95
+ // An example is "type='signal',sender='org.freedesktop.DBus', interface='org.freedesktop.DBus',member='Foo', path='/bar/foo',destination=':452345.34'" ...
96
+ var bus = this.$parent.service.bus;
97
+ var signalFullName = bus.mangle(this.$parent.name, this.$name, signame);
98
+ if (!bus.signals.listeners(signalFullName).length) {
99
+ // This is the first time, so call addMatch
100
+ var match = getMatchRule(this.$parent.name, this.$name, signame);
101
+ bus.addMatch(match, function(err) {
102
+ if (err) throw new Error(err);
103
+ bus.signals.on(signalFullName, this.$getSigHandler(callback));
104
+ }.bind(this));
105
+ } else {
106
+ // The match is already there, just add event listener
107
+ bus.signals.on(signalFullName, this.$getSigHandler(callback));
108
+ }
109
+ }
110
+ DBusInterface.prototype.removeListener = DBusInterface.prototype.off = function(signame, callback) {
111
+ var bus = this.$parent.service.bus;
112
+ var signalFullName = bus.mangle(this.$parent.name, this.$name, signame);
113
+ bus.signals.removeListener( signalFullName, this.$getSigHandler(callback) );
114
+ if (!bus.signals.listeners(signalFullName).length) {
115
+ // There is no event handlers for this match
116
+ var match = getMatchRule(this.$parent.name, this.$name, signame);
117
+ bus.removeMatch(match, function(err) {
118
+ if (err) throw new Error(err);
119
+ // Now it is safe to empty these arrays
120
+ this.$callbacks.length = 0;
121
+ this.$sigHandlers.length = 0;
122
+ }.bind(this));
123
+ }
124
+ }
125
+ DBusInterface.prototype.$createMethod = function(mName, signature)
126
+ {
127
+ this.$methods[mName] = signature;
128
+ this[mName] = function() { this.$callMethod(mName, arguments); }
129
+ }
130
+ DBusInterface.prototype.$callMethod = function(mName, args)
131
+ {
132
+ var bus = this.$parent.service.bus;
133
+ if (!Array.isArray(args)) args = Array.from(args); // Array.prototype.slice.apply(args)
134
+ var callback =
135
+ typeof args[args.length - 1] === 'function'
136
+ ? args.pop()
137
+ : function() {};
138
+ var msg = {
139
+ destination: this.$parent.service.name,
140
+ path: this.$parent.name,
141
+ interface: this.$name,
142
+ member: mName
143
+ };
144
+ if (this.$methods[mName] !== '') {
145
+ msg.signature = this.$methods[mName];
146
+ msg.body = args;
147
+ }
148
+ bus.invoke(msg, callback);
149
+ }
150
+ DBusInterface.prototype.$createProp = function(propName, propType, propAccess)
151
+ {
152
+ this.$properties[propName] = { type: propType, access: propAccess };
153
+ Object.defineProperty(this, propName, {
154
+ enumerable: true,
155
+ get: () => callback => this.$readProp(propName, callback),
156
+ set: function(val) { this.$writeProp(propName, val) }
157
+ });
158
+ }
159
+ DBusInterface.prototype.$readProp = function(propName, callback)
160
+ {
161
+ var bus = this.$parent.service.bus;
162
+ bus.invoke(
163
+ {
164
+ destination: this.$parent.service.name,
165
+ path: this.$parent.name,
166
+ interface: 'org.freedesktop.DBus.Properties',
167
+ member: 'Get',
168
+ signature: 'ss',
169
+ body: [this.$name, propName]
170
+ },
171
+ function(err, val) {
172
+ if (err) {
173
+ callback(err);
174
+ } else {
175
+ var signature = val[0];
176
+ if (signature.length === 1) {
177
+ callback(err, val[1][0]);
178
+ } else {
179
+ callback(err, val[1]);
180
+ }
181
+ }
182
+ }
183
+ );
184
+ }
185
+ DBusInterface.prototype.$writeProp = function(propName, val)
186
+ {
187
+ var bus = this.$parent.service.bus;
188
+ bus.invoke({
189
+ destination: this.$parent.service.name,
190
+ path: this.$parent.name,
191
+ interface: 'org.freedesktop.DBus.Properties',
192
+ member: 'Set',
193
+ signature: 'ssv',
194
+ body: [this.$name, propName, [this.$properties[propName].type, val]]
195
+ });
196
+ }
197
+
198
+
199
+ function getMatchRule(objName, ifName, signame) {
200
+ return `type='signal',path='${objName}',interface='${ifName}',member='${signame}'`;
201
+ }
@@ -0,0 +1,108 @@
1
+ const assert = require('assert');
2
+
3
+ const parseSignature = require('./signature');
4
+ const put = require('put');
5
+ const Marshallers = require('./marshallers');
6
+ const align = require('./align').align;
7
+
8
+ module.exports = function marshall(signature, data, offset) {
9
+ if (typeof offset === 'undefined') offset = 0;
10
+ var tree = parseSignature(signature);
11
+ if (!Array.isArray(data) || data.length !== tree.length) {
12
+ throw new Error(
13
+ `message body does not match message signature. Body:${JSON.stringify(
14
+ data
15
+ )}, signature:${signature}`
16
+ );
17
+ }
18
+ var putstream = put();
19
+ putstream._offset = offset;
20
+ var buf = writeStruct(putstream, tree, data).buffer();
21
+ return buf;
22
+ };
23
+
24
+ // TODO: serialise JS objects as a{sv}
25
+ //function writeHash(ps, treeKey, treeVal, data) {
26
+ //
27
+ //}
28
+
29
+ function writeStruct(ps, tree, data) {
30
+ if (tree.length !== data.length) {
31
+ throw new Error('Invalid struct data');
32
+ }
33
+ for (var i = 0; i < tree.length; ++i) {
34
+ write(ps, tree[i], data[i]);
35
+ }
36
+ return ps;
37
+ }
38
+
39
+ function write(ps, ele, data) {
40
+ switch (ele.type) {
41
+ case '(':
42
+ case '{':
43
+ align(ps, 8);
44
+ writeStruct(ps, ele.child, data);
45
+ break;
46
+ case 'a':
47
+ // array serialisation:
48
+ // length of array body aligned at 4 byte boundary
49
+ // (optional 4 bytes to align first body element on 8-byte boundary if element
50
+ // body
51
+ var arrPut = put();
52
+ arrPut._offset = ps._offset;
53
+ var _offset = arrPut._offset;
54
+ writeSimple(arrPut, 'u', 0); // array length placeholder
55
+ var lengthOffset = arrPut._offset - 4 - _offset;
56
+ // we need to alighn here because alignment is not included in array length
57
+ if (['x', 't', 'd', '{', '('].indexOf(ele.child[0].type) !== -1)
58
+ align(arrPut, 8);
59
+ var startOffset = arrPut._offset;
60
+ for (var i = 0; i < data.length; ++i)
61
+ write(arrPut, ele.child[0], data[i]);
62
+ var arrBuff = arrPut.buffer();
63
+ var length = arrPut._offset - startOffset;
64
+ // lengthOffset in the range 0 to 3 depending on number of align bytes padded _before_ arrayLength
65
+ arrBuff.writeUInt32LE(length, lengthOffset);
66
+ ps.put(arrBuff);
67
+ ps._offset += arrBuff.length;
68
+ break;
69
+ case 'v':
70
+ // TODO: allow serialisation of simple types as variants, e. g 123 -> ['u', 123], true -> ['b', 1], 'abc' -> ['s', 'abc']
71
+ assert.equal(data.length, 2, 'variant data should be [signature, data]');
72
+ var signatureEle = {
73
+ type: 'g',
74
+ child: []
75
+ };
76
+ write(ps, signatureEle, data[0]);
77
+ var tree = parseSignature(data[0]);
78
+ assert(tree.length === 1);
79
+ write(ps, tree[0], data[1]);
80
+ break;
81
+ default:
82
+ return writeSimple(ps, ele.type, data);
83
+ }
84
+ }
85
+
86
+ var stringTypes = ['g', 'o', 's'];
87
+
88
+ function writeSimple(ps, type, data) {
89
+ if (typeof data === 'undefined')
90
+ throw new Error(
91
+ "Serialisation of JS 'undefined' type is not supported by d-bus"
92
+ );
93
+ if (data === null)
94
+ throw new Error('Serialisation of null value is not supported by d-bus');
95
+
96
+ if (Buffer.isBuffer(data)) data = data.toString(); // encoding?
97
+ if (stringTypes.indexOf(type) !== -1 && typeof data !== 'string') {
98
+ throw new Error(
99
+ `Expected string or buffer argument, got ${JSON.stringify(
100
+ data
101
+ )} of type '${type}'`
102
+ );
103
+ }
104
+
105
+ var simpleMarshaller = Marshallers.MakeSimpleMarshaller(type);
106
+ simpleMarshaller.marshall(ps, data);
107
+ return ps;
108
+ }