diff --git a/package-lock.json b/package-lock.json
index c9386af9d..5dfe5c6cd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,9 +15,11 @@
"debug": "^4.3.6",
"fast-srp-hap": "^2.0.4",
"futoin-hkdf": "^1.5.3",
+ "long": "^5.2.3",
"node-persist": "^0.0.12",
"source-map-support": "^0.5.21",
- "tweetnacl": "^1.0.3"
+ "tweetnacl": "^1.0.3",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@antfu/eslint-config": "^3.0.0",
@@ -27,6 +29,7 @@
"@types/plist": "^3.0.5",
"@types/semver": "^7.3.7",
"@types/source-map-support": "^0.5.10",
+ "@types/xml2js": "^0.4.14",
"@vitest/coverage-v8": "^2.0.5",
"axios": "^1.7.5",
"commander": "^12.1.0",
@@ -1725,6 +1728,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/xml2js": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
+ "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
@@ -5258,6 +5271,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
+ "license": "Apache-2.0"
+ },
"node_modules/loupe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
diff --git a/package.json b/package.json
index 94cdfb505..a34a8ce8c 100644
--- a/package.json
+++ b/package.json
@@ -61,9 +61,11 @@
"debug": "^4.3.6",
"fast-srp-hap": "^2.0.4",
"futoin-hkdf": "^1.5.3",
+ "long": "^5.2.3",
"node-persist": "^0.0.12",
"source-map-support": "^0.5.21",
- "tweetnacl": "^1.0.3"
+ "tweetnacl": "^1.0.3",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@antfu/eslint-config": "^3.0.0",
@@ -73,6 +75,7 @@
"@types/plist": "^3.0.5",
"@types/semver": "^7.3.7",
"@types/source-map-support": "^0.5.10",
+ "@types/xml2js": "^0.4.14",
"@vitest/coverage-v8": "^2.0.5",
"axios": "^1.7.5",
"commander": "^12.1.0",
diff --git a/src/lib/Advertiser.ts b/src/lib/Advertiser.ts
index 08377f8f3..dbdb902bc 100644
--- a/src/lib/Advertiser.ts
+++ b/src/lib/Advertiser.ts
@@ -12,10 +12,10 @@ import { createHash } from 'node:crypto'
import { EventEmitter } from 'node:events'
import { getResponder, ServiceEvent, ServiceType } from '@homebridge/ciao'
-import { systemBus } from '@homebridge/dbus-native'
import bonjour from 'bonjour-hap'
import createDebug from 'debug'
+import { systemBus } from './dbus/index.js'
import { PromiseTimeout } from './util/promise-utils.js'
const debug = createDebug('HAP-NodeJS:Advertiser')
diff --git a/src/lib/dbus/align.ts b/src/lib/dbus/align.ts
new file mode 100644
index 000000000..e61342d49
--- /dev/null
+++ b/src/lib/dbus/align.ts
@@ -0,0 +1,12 @@
+import { Buffer } from 'safe-buffer'
+
+export default function align(ps: any, n: number) {
+ const pad = n - (ps._offset % n)
+ if (pad === 0 || pad === n) {
+ return
+ }
+ // TODO: write8(0) in a loop (3 to 7 times here) could be more efficient
+ const padBuff = Buffer.alloc(pad)
+ ps.put(Buffer.from(padBuff))
+ ps._offset += pad
+}
diff --git a/src/lib/dbus/bus.ts b/src/lib/dbus/bus.ts
new file mode 100644
index 000000000..a87bbfbcc
--- /dev/null
+++ b/src/lib/dbus/bus.ts
@@ -0,0 +1,248 @@
+// @ts-nocheck
+import { EventEmitter } from 'node:events'
+
+import constants from './constants.js'
+import { introspectBus } from './introspect.js'
+import stdDbusIfaces from './stdifaces.js'
+
+export default function MessageBus(conn, opts) {
+ if (!(this instanceof MessageBus)) {
+ return new MessageBus(conn)
+ }
+ if (!opts) {
+ opts = {}
+ }
+
+ const self = this // eslint-disable-line ts/no-this-alias
+ this.connection = conn
+ this.serial = 1
+ this.cookies = {} // TODO: rename to methodReturnHandlers
+ this.methodCallHandlers = {}
+ this.signals = new EventEmitter()
+ this.exportedObjects = {}
+
+ this.invoke = function (msg, callback) {
+ if (!msg.type) {
+ msg.type = constants.messageType.methodCall
+ }
+ msg.serial = self.serial++
+ this.cookies[msg.serial] = callback
+ self.connection.message(msg)
+ }
+
+ this.invokeDbus = function (msg, callback) {
+ if (!msg.path) {
+ msg.path = '/org/freedesktop/DBus'
+ }
+ if (!msg.destination) {
+ msg.destination = 'org.freedesktop.DBus'
+ }
+ if (!msg.interface) {
+ msg.interface = 'org.freedesktop.DBus'
+ }
+ self.invoke(msg, callback)
+ }
+
+ this.mangle = function (path, iface, member) {
+ const obj = {}
+ if (typeof path === 'object') {
+ // handle one argument case mangle(msg)
+ obj.path = path.path
+ obj.interface = path.interface
+ obj.member = path.member
+ } else {
+ obj.path = path
+ obj.interface = iface
+ obj.member = member
+ }
+ return JSON.stringify(obj)
+ }
+
+ // Warning: errorName must respect the same rules as interface names (must contain a dot)
+ this.sendError = function (msg, errorName, errorText) {
+ const reply = {
+ type: constants.messageType.error,
+ serial: self.serial++,
+ replySerial: msg.serial,
+ destination: msg.sender,
+ errorName,
+ signature: 's',
+ body: [errorText],
+ }
+ this.connection.message(reply)
+ }
+
+ // route reply/error
+ this.connection.on('message', (msg) => {
+ function invoke(impl, func, resultSignature) {
+ Promise.resolve()
+ .then(() => {
+ return func.apply(impl, (msg.body || []).concat(msg))
+ })
+ .then(
+ (methodReturnResult) => {
+ const methodReturnReply = {
+ type: constants.messageType.methodReturn,
+ serial: self.serial++,
+ destination: msg.sender,
+ replySerial: msg.serial,
+ }
+ if (methodReturnResult !== null) {
+ methodReturnReply.signature = resultSignature
+ methodReturnReply.body = [methodReturnResult]
+ }
+ self.connection.message(methodReturnReply)
+ },
+ (e) => {
+ self.sendError(
+ msg,
+ e.dbusName || 'org.freedesktop.DBus.Error.Failed',
+ e.message || '',
+ )
+ },
+ )
+ }
+
+ let handler
+ if (msg.type === constants.messageType.methodReturn || msg.type === constants.messageType.error) {
+ handler = self.cookies[msg.replySerial]
+ if (handler) {
+ delete self.cookies[msg.replySerial]
+ const props = {
+ connection: self.connection,
+ bus: self,
+ message: msg,
+ signature: msg.signature,
+ }
+ let args = msg.body || []
+ if (msg.type === constants.messageType.methodReturn) {
+ args = [null].concat(args) // first argument - no errors, null
+ handler.apply(props, args) // body as array of arguments
+ } else {
+ handler.call(props, { name: msg.errorName, message: args }) // body as first argument
+ }
+ }
+ } else if (msg.type === constants.messageType.signal) {
+ self.signals.emit(self.mangle(msg), msg.body, msg.signature)
+ } else {
+ // methodCall
+ if (stdDbusIfaces(msg, self)) {
+ return
+ }
+
+ // exported interfaces handlers
+ const obj = self.exportedObjects[msg.path]
+ let iface
+
+ if (obj) {
+ iface = obj[msg.interface]
+ if (iface) {
+ // now we are ready to serve msg.member
+ const impl = iface[1]
+ const func = impl[msg.member]
+ if (!func) {
+ self.sendError(
+ msg,
+ 'org.freedesktop.DBus.Error.UnknownMethod',
+ `Method "${msg.member}" on interface "${msg.interface}" doesn't exist`,
+ )
+ return
+ }
+ // TODO safety check here
+ const resultSignature = iface[0].methods[msg.member][1]
+ invoke(impl, func, resultSignature)
+ return
+ } else {
+ console.error(`Interface ${msg.interface} is not supported`)
+ // TODO: respond with standard dbus error
+ }
+ }
+
+ // setMethodCall handlers
+ handler = self.methodCallHandlers[self.mangle(msg)]
+ if (handler) {
+ invoke(null, handler[0], handler[1])
+ } else {
+ self.sendError(
+ msg,
+ 'org.freedesktop.DBus.Error.UnknownService',
+ 'Uh oh oh',
+ )
+ }
+ }
+ })
+
+ // register name
+ if (opts.direct !== true) {
+ this.invokeDbus({ member: 'Hello' }, (err, name) => {
+ if (err) {
+ throw new Error(err)
+ }
+ self.name = name
+ })
+ } else {
+ self.name = null
+ }
+
+ // eslint-disable-next-line unicorn/consistent-function-scoping
+ function DBusObject(name, service) {
+ this.name = name
+ this.service = service
+ this.as = function (name) {
+ return this.proxy[name]
+ }
+ }
+
+ function DBusService(name, bus) {
+ this.name = name
+ this.bus = bus
+ this.getObject = function (name, callback) {
+ if (name === undefined) {
+ return callback(new Error('Object name is null or undefined'))
+ }
+ const obj = new DBusObject(name, this)
+ introspectBus(obj, (err, ifaces, nodes) => {
+ if (err) {
+ return callback(err)
+ }
+ obj.proxy = ifaces
+ obj.nodes = nodes
+ callback(null, obj)
+ })
+ }
+
+ this.getInterface = function (objName, ifaceName, callback) {
+ this.getObject(objName, (err, obj) => {
+ if (err) {
+ return callback(err)
+ }
+ callback(null, obj.as(ifaceName))
+ })
+ }
+ }
+
+ this.getService = function (name) {
+ return new DBusService(name, this)
+ }
+
+ this.getObject = function (path, name, callback) {
+ const service = this.getService(path)
+ return service.getObject(name, callback)
+ }
+
+ this.getInterface = function (path, objname, name, callback) {
+ return this.getObject(path, objname, (err, obj) => {
+ if (err) {
+ return callback(err)
+ }
+ callback(null, obj.as(name))
+ })
+ }
+
+ this.removeMatch = function (match, callback) {
+ this.invokeDbus(
+ { member: 'RemoveMatch', signature: 's', body: [match] },
+ callback,
+ )
+ }
+}
diff --git a/src/lib/dbus/constants.ts b/src/lib/dbus/constants.ts
new file mode 100644
index 000000000..a641afc9d
--- /dev/null
+++ b/src/lib/dbus/constants.ts
@@ -0,0 +1,54 @@
+export default {
+ messageType: {
+ invalid: 0,
+ methodCall: 1,
+ methodReturn: 2,
+ error: 3,
+ signal: 4,
+ },
+
+ headerTypeName: [
+ null,
+ 'path',
+ 'interface',
+ 'member',
+ 'errorName',
+ 'replySerial',
+ 'destination',
+ 'sender',
+ 'signature',
+ ],
+
+ // TODO: merge to single hash? e.g path -> [1, 'o']
+ fieldSignature: {
+ path: 'o',
+ interface: 's',
+ member: 's',
+ errorName: 's',
+ replySerial: 'u',
+ destination: 's',
+ sender: 's',
+ signature: 'g',
+ },
+ headerTypeId: {
+ path: 1,
+ interface: 2,
+ member: 3,
+ errorName: 4,
+ replySerial: 5,
+ destination: 6,
+ sender: 7,
+ signature: 8,
+ },
+ protocolVersion: 1,
+ flags: {
+ noReplyExpected: 1,
+ noAutoStart: 2,
+ },
+ endianness: {
+ le: 108,
+ be: 66,
+ },
+ messageSignature: 'yyyyuua(yv)',
+ defaultAuthMethods: ['EXTERNAL', 'DBUS_COOKIE_SHA1', 'ANONYMOUS'],
+}
diff --git a/src/lib/dbus/dbus-buffer.ts b/src/lib/dbus/dbus-buffer.ts
new file mode 100644
index 000000000..f020941ad
--- /dev/null
+++ b/src/lib/dbus/dbus-buffer.ts
@@ -0,0 +1,189 @@
+// @ts-nocheck
+import Long from 'long'
+
+import parseSignature from './signature.js'
+
+// Buffer + position + global start position ( used in alignment )
+export default function DBusBuffer(buffer, startPos, options) {
+ if (typeof options !== 'object') {
+ options = { ayBuffer: true, ReturnLongjs: false }
+ } else if (options.ayBuffer === undefined) {
+ // default settings object
+ options.ayBuffer = true // enforce truthy default props
+ }
+ this.options = options
+ this.buffer = buffer
+ this.startPos = startPos || 0
+ this.pos = 0
+}
+
+DBusBuffer.prototype.align = function (power) {
+ const allbits = (1 << power) - 1
+ const paddedOffset = ((this.pos + this.startPos + allbits) >> power) << power
+ this.pos = paddedOffset - this.startPos
+}
+
+DBusBuffer.prototype.readInt8 = function () {
+ this.pos++
+ return this.buffer[this.pos - 1]
+}
+
+DBusBuffer.prototype.readSInt16 = function () {
+ this.align(1)
+ const res = this.buffer.readInt16LE(this.pos)
+ this.pos += 2
+ return res
+}
+
+DBusBuffer.prototype.readInt16 = function () {
+ this.align(1)
+ const res = this.buffer.readUInt16LE(this.pos)
+ this.pos += 2
+ return res
+}
+
+DBusBuffer.prototype.readSInt32 = function () {
+ this.align(2)
+ const res = this.buffer.readInt32LE(this.pos)
+ this.pos += 4
+ return res
+}
+
+DBusBuffer.prototype.readInt32 = function () {
+ this.align(2)
+ const res = this.buffer.readUInt32LE(this.pos)
+ this.pos += 4
+ return res
+}
+
+DBusBuffer.prototype.readDouble = function () {
+ this.align(3)
+ const res = this.buffer.readDoubleLE(this.pos)
+ this.pos += 8
+ return res
+}
+
+DBusBuffer.prototype.readString = function (len) {
+ if (len === 0) {
+ this.pos++
+ return ''
+ }
+ const res = this.buffer.toString('utf8', this.pos, this.pos + len)
+ this.pos += len + 1 // dbus strings are always zero-terminated ('s' and 'g' types)
+ return res
+}
+
+DBusBuffer.prototype.readTree = function readTree(tree) {
+ switch (tree.type) {
+ case '(':
+ case '{':
+ case 'r':
+ this.align(3)
+ return this.readStruct(tree.child)
+ case 'a': {
+ if (!tree.child || tree.child.length !== 1) {
+ throw new Error('Incorrect array element signature')
+ }
+ const arrayBlobLength = this.readInt32()
+ return this.readArray(tree.child[0], arrayBlobLength)
+ }
+ case 'v':
+ return this.readVariant()
+ default:
+ return this.readSimpleType(tree.type)
+ }
+}
+
+DBusBuffer.prototype.read = function read(signature) {
+ const tree = parseSignature(signature)
+ return this.readStruct(tree)
+}
+
+DBusBuffer.prototype.readVariant = function readVariant() {
+ const signature = this.readSimpleType('g')
+ const tree = parseSignature(signature)
+ return [tree, this.readStruct(tree)]
+}
+
+DBusBuffer.prototype.readStruct = function readStruct(struct) {
+ const result = []
+ for (let i = 0; i < struct.length; ++i) {
+ result.push(this.readTree(struct[i]))
+ }
+ return result
+}
+
+DBusBuffer.prototype.readArray = function readArray(eleType, arrayBlobSize) {
+ const start = this.pos
+
+ // special case: treat ay as Buffer
+ if (eleType.type === 'y' && this.options.ayBuffer) {
+ this.pos += arrayBlobSize
+ return this.buffer.slice(start, this.pos)
+ }
+
+ // end of array is start of first element + array size
+ // we need to add 4 bytes if not on 8-byte boundary
+ // and array element needs 8 byte alignment
+ if (['x', 't', 'd', '{', '(', 'r'].includes(eleType.type)) {
+ this.align(3)
+ }
+ const end = this.pos + arrayBlobSize
+ const result = []
+ while (this.pos < end) result.push(this.readTree(eleType))
+ return result
+}
+
+DBusBuffer.prototype.readSimpleType = function readSimpleType(t) {
+ let data, len, word0, word1
+ switch (t) {
+ case 'y':
+ return this.readInt8()
+ case 'b':
+ // TODO: spec says that true is strictly 1 and false is strictly 0
+ // shold we error (or warn?) when non 01 values?
+ return !!this.readInt32()
+ case 'n':
+ return this.readSInt16()
+ case 'q':
+ return this.readInt16()
+ case 'u':
+ return this.readInt32()
+ case 'i':
+ return this.readSInt32()
+ case 'g':
+ len = this.readInt8()
+ return this.readString(len)
+ case 's':
+ case 'o':
+ len = this.readInt32()
+ return this.readString(len)
+ // TODO: validate object path here
+ // if (t === 'o' && !isValidObjectPath(str))
+ // throw new Error('string is not a valid object path'));
+ case 'x':
+ // signed
+ this.align(3)
+ word0 = this.readInt32()
+ word1 = this.readInt32()
+ data = Long.fromBits(word0, word1, false)
+ if (this.options.ReturnLongjs) {
+ return data
+ }
+ return data.toNumber() // convert to number (good up to 53 bits)
+ case 't':
+ // unsigned
+ this.align(3)
+ word0 = this.readInt32()
+ word1 = this.readInt32()
+ data = Long.fromBits(word0, word1, true)
+ if (this.options.ReturnLongjs) {
+ return data
+ }
+ return data.toNumber() // convert to number (good up to 53 bits)
+ case 'd':
+ return this.readDouble()
+ default:
+ throw new Error(`Unsupported type: ${t}`)
+ }
+}
diff --git a/src/lib/dbus/handshake.ts b/src/lib/dbus/handshake.ts
new file mode 100644
index 000000000..02c84be3d
--- /dev/null
+++ b/src/lib/dbus/handshake.ts
@@ -0,0 +1,148 @@
+// @ts-nocheck
+import crypto from 'node:crypto'
+import fs from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+
+import { Buffer } from 'safe-buffer'
+
+import constants from './constants.js'
+import readLine from './readline.js'
+
+function sha1(input) {
+ const shasum = crypto.createHash('sha1')
+ shasum.update(input)
+ return shasum.digest('hex')
+}
+
+function getUserHome() {
+ return process.env[process.platform.match(/\$win/) ? 'USERPROFILE' : 'HOME']
+}
+
+function getCookie(context, id, cb) {
+ // http://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha
+ const dirname = path.join(getUserHome(), '.dbus-keyrings')
+ // > There is a default context, "org_freedesktop_general" that's used by servers that do not specify otherwise.
+ if (context.length === 0) {
+ context = 'org_freedesktop_general'
+ }
+
+ const filename = path.join(dirname, context)
+ // check it's not writable by others and readable by user
+ fs.stat(dirname, (err, stat) => {
+ if (err) {
+ return cb(err)
+ }
+ if (stat.mode & 0o22) {
+ return cb(
+ new Error(
+ 'User keyrings directory is writeable by other users. Aborting authentication',
+ ),
+ )
+ }
+ // eslint-disable-next-line no-prototype-builtins
+ if (process.hasOwnProperty('getuid') && stat.uid !== process.getuid()) {
+ return cb(
+ new Error(
+ 'Keyrings directory is not owned by the current user. Aborting authentication!',
+ ),
+ )
+ }
+ fs.readFile(filename, 'ascii', (err, keyrings) => {
+ if (err) {
+ return cb(err)
+ }
+ const lines = keyrings.split('\n')
+ for (let l = 0; l < lines.length; ++l) {
+ const data = lines[l].split(' ')
+ if (id === data[0]) {
+ return cb(null, data[2])
+ }
+ }
+ return cb(new Error('cookie not found'))
+ })
+ })
+}
+
+function hexlify(input) {
+ return Buffer.from(input.toString(), 'ascii').toString('hex')
+}
+
+export default function auth(stream, opts, cb) {
+ // filter used to make a copy so we don't accidentally change opts data
+ let authMethods
+ if (opts.authMethods) {
+ authMethods = opts.authMethods
+ } else {
+ authMethods = constants.defaultAuthMethods
+ }
+ stream.write('\0')
+ tryAuth(stream, authMethods.slice(), cb)
+}
+
+function tryAuth(stream, methods, cb) {
+ if (methods.length === 0) {
+ return cb(new Error('No authentication methods left to try'))
+ }
+
+ const authMethod = methods.shift()
+ // eslint-disable-next-line no-prototype-builtins
+ const uid = process.hasOwnProperty('getuid') ? process.getuid() : 0
+ const id = hexlify(uid)
+
+ function beginOrNextAuth() {
+ readLine(stream, (line) => {
+ const ok = line.toString('ascii').match(/^([A-Z]+) (.*)/i)
+ if (ok && ok[1] === 'OK') {
+ stream.write('BEGIN\r\n')
+ return cb(null, ok[2]) // ok[2] = guid. Do we need it?
+ } else {
+ // TODO: parse error!
+ if (!methods.empty) {
+ tryAuth(stream, methods, cb)
+ } else {
+ return cb(line)
+ }
+ }
+ })
+ }
+
+ switch (authMethod) {
+ case 'EXTERNAL':
+ stream.write(`AUTH ${authMethod} ${id}\r\n`)
+ beginOrNextAuth()
+ break
+ case 'DBUS_COOKIE_SHA1':
+ stream.write(`AUTH ${authMethod} ${id}\r\n`)
+ readLine(stream, (line) => {
+ const data = Buffer.from(line.toString().split(' ')[1].trim(), 'hex')
+ .toString()
+ .split(' ')
+ const cookieContext = data[0]
+ const cookieId = data[1]
+ const serverChallenge = data[2]
+ // any random 16 bytes should work, sha1(rnd) to make it simpler
+ const clientChallenge = crypto.randomBytes(16).toString('hex')
+ getCookie(cookieContext, cookieId, (err, cookie) => {
+ if (err) {
+ return cb(err)
+ }
+ const response = sha1(
+ [serverChallenge, clientChallenge, cookie].join(':'),
+ )
+ const reply = hexlify(clientChallenge + response)
+ stream.write(`DATA ${reply}\r\n`)
+ beginOrNextAuth()
+ })
+ })
+ break
+ case 'ANONYMOUS':
+ stream.write('AUTH ANONYMOUS \r\n')
+ beginOrNextAuth()
+ break
+ default:
+ console.error(`Unsupported auth method: ${authMethod}`)
+ beginOrNextAuth()
+ break
+ }
+}
diff --git a/src/lib/dbus/index.ts b/src/lib/dbus/index.ts
new file mode 100644
index 000000000..078c7a088
--- /dev/null
+++ b/src/lib/dbus/index.ts
@@ -0,0 +1,143 @@
+// @ts-nocheck
+import { EventEmitter } from 'node:events'
+import net from 'node:net'
+import process from 'node:process'
+
+import MessageBus from './bus.js'
+import clientHandshake from './handshake.js'
+import { marshalMessage, unmarshalMessages } from './message.js'
+
+function createStream(opts) {
+ if (opts.stream) {
+ return opts.stream
+ }
+ let host = opts.host
+ let port = opts.port
+ const socket = opts.socket
+ if (socket) {
+ return net.createConnection(socket)
+ }
+ if (port) {
+ return net.createConnection(port, host)
+ }
+
+ const busAddress = opts.busAddress || process.env.DBUS_SESSION_BUS_ADDRESS
+ if (!busAddress) {
+ throw new Error('unknown bus address')
+ }
+
+ const addresses = busAddress.split(';')
+ for (let i = 0; i < addresses.length; ++i) {
+ const address = addresses[i]
+ const familyParams = address.split(':')
+ const family = familyParams[0]
+ const params = {}
+ familyParams[1].split(',').map((p) => { // eslint-disable-line array-callback-return
+ const keyVal = p.split('=')
+ params[keyVal[0]] = keyVal[1]
+ })
+
+ try {
+ switch (family.toLowerCase()) {
+ case 'tcp':
+ host = params.host || 'localhost'
+ port = params.port
+ return net.createConnection(port, host)
+ case 'unix':
+ if (params.socket) {
+ return net.createConnection(params.socket)
+ }
+ if (params.path) {
+ return net.createConnection(params.path)
+ }
+ throw new Error(
+ 'not enough parameters for \'unix\' connection - you need to specify \'socket\' or \'abstract\' or \'path\' parameter',
+ )
+ default:
+ throw new Error(`unknown address type:${family}`)
+ }
+ } catch (e) {
+ if (i < addresses.length - 1) {
+ console.warn(e.message)
+ } else {
+ throw e
+ }
+ }
+ }
+}
+
+function createConnection(opts) {
+ const self = new EventEmitter()
+ if (!opts) {
+ opts = {}
+ }
+ const stream = (self.stream = createStream(opts))
+ stream.setNoDelay()
+
+ stream.on('error', (err) => {
+ // forward network and stream errors
+ self.emit('error', err)
+ })
+
+ stream.on('end', () => {
+ self.emit('end')
+ self.message = function () {
+ console.warn('Didn\'t write bytes to closed stream')
+ }
+ })
+
+ self.end = function () {
+ stream.end()
+ return self
+ }
+
+ clientHandshake(stream, opts, (error, guid) => {
+ if (error) {
+ return self.emit('error', error)
+ }
+ self.guid = guid
+ self.emit('connect')
+ unmarshalMessages(
+ stream,
+ (message) => {
+ self.emit('message', message)
+ },
+ opts,
+ )
+ })
+
+ self._messages = []
+
+ // pre-connect version, buffers all messages. replaced after connect
+ self.message = function (msg) {
+ self._messages.push(msg)
+ }
+
+ self.once('connect', () => {
+ self.state = 'connected'
+ for (let i = 0; i < self._messages.length; ++i) {
+ stream.write(marshalMessage(self._messages[i]))
+ }
+ self._messages.length = 0
+
+ // no need to buffer once connected
+ self.message = function (msg) {
+ stream.write(marshalMessage(msg))
+ }
+ })
+
+ return self
+}
+
+function createClient(params) {
+ const connection = createConnection(params || {})
+ return new MessageBus(connection, params || {})
+}
+
+export function systemBus() {
+ return createClient({
+ busAddress:
+ process.env.DBUS_SYSTEM_BUS_ADDRESS
+ || 'unix:path=/var/run/dbus/system_bus_socket',
+ })
+}
diff --git a/src/lib/dbus/introspect.ts b/src/lib/dbus/introspect.ts
new file mode 100644
index 000000000..498edaef3
--- /dev/null
+++ b/src/lib/dbus/introspect.ts
@@ -0,0 +1,204 @@
+// @ts-nocheck
+import xml2js from 'xml2js'
+
+export function introspectBus(obj, callback) {
+ const bus = obj.service.bus
+ bus.invoke(
+ {
+ destination: obj.service.name,
+ path: obj.name,
+ interface: 'org.freedesktop.DBus.Introspectable',
+ member: 'Introspect',
+ },
+ (err, xml) => {
+ processXML(err, xml, obj, callback)
+ },
+ )
+}
+
+export function processXML(err, xml, obj, callback) {
+ if (err) {
+ return callback(err)
+ }
+ const parser = new xml2js.Parser()
+ parser.parseString(xml, (err, result) => {
+ if (err) {
+ return callback(err)
+ }
+ if (!result.node) {
+ throw new Error('No root XML node')
+ }
+ result = result.node // unwrap the root node
+ // If no interface, try first sub node?
+ if (!result.interface) {
+ if (result.node && result.node.length > 0 && result.node[0].$) {
+ const subObj = Object.assign(obj, {})
+ if (subObj.name.slice(-1) !== '/') {
+ subObj.name += '/'
+ }
+ subObj.name += result.node[0].$.name
+ return module.exports.introspectBus(subObj, callback)
+ }
+ return callback(new Error('No such interface found'))
+ }
+ const proxy = {}
+ const nodes = []
+ let ifaceName, method, property, iface, arg, signature, currentIface
+ const ifaces = result.interface
+ const xmlnodes = result.node || []
+
+ for (let n = 1; n < xmlnodes.length; ++n) {
+ // Start at 1 because we want to skip the root node
+ nodes.push(xmlnodes[n].$.name)
+ }
+
+ for (let i = 0; i < ifaces.length; ++i) {
+ iface = ifaces[i]
+ ifaceName = iface.$.name
+ currentIface = proxy[ifaceName] = new DBusInterface(obj, ifaceName)
+
+ for (let m = 0; iface.method && m < iface.method.length; ++m) {
+ method = iface.method[m]
+ signature = ''
+ const methodName = method.$.name
+ for (let a = 0; method.arg && a < method.arg.length; ++a) {
+ arg = method.arg[a].$
+ if (arg.direction === 'in') {
+ signature += arg.type
+ }
+ }
+ // add method
+ currentIface.$createMethod(methodName, signature)
+ }
+ for (let p = 0; iface.property && p < iface.property.length; ++p) {
+ property = iface.property[p]
+ currentIface.$createProp(
+ property.$.name,
+ property.$.type,
+ property.$.access,
+ )
+ }
+ // TODO: introspect signals
+ }
+ callback(null, proxy, nodes)
+ })
+}
+
+function DBusInterface(parent_obj, ifname) {
+ // 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
+ // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
+ this.$parent = parent_obj // parent DbusObject
+ this.$name = ifname // string interface name
+}
+
+DBusInterface.prototype.$getSigHandler = function (callback) {
+ let index = this.$callbacks.indexOf(callback)
+ if (index === -1) {
+ index = this.$callbacks.push(callback) - 1
+ this.$sigHandlers[index] = function (messageBody) {
+ callback(...messageBody)
+ }
+ }
+ return this.$sigHandlers[index]
+}
+
+DBusInterface.prototype.removeListener = DBusInterface.prototype.off = function (signame, callback) {
+ const bus = this.$parent.service.bus
+ const signalFullName = bus.mangle(this.$parent.name, this.$name, signame)
+ bus.signals.removeListener(signalFullName, this.$getSigHandler(callback))
+ if (!bus.signals.listeners(signalFullName).length) {
+ // There is no event handlers for this match
+ const match = getMatchRule(this.$parent.name, this.$name, signame)
+ bus.removeMatch(
+ match,
+ (err) => {
+ if (err) {
+ throw new Error(err)
+ }
+ // Now it is safe to empty these arrays
+ this.$callbacks.length = 0
+ this.$sigHandlers.length = 0
+ },
+ )
+ }
+}
+
+DBusInterface.prototype.$createMethod = function (mName, signature) {
+ this.$methods[mName] = signature
+ this[mName] = function (...args) {
+ this.$callMethod(mName, args)
+ }
+}
+
+DBusInterface.prototype.$callMethod = function (mName, args) {
+ const bus = this.$parent.service.bus
+ if (!Array.isArray(args)) {
+ args = Array.from(args) // Array.prototype.slice.apply(args)
+ }
+ const callback
+ = typeof args[args.length - 1] === 'function' ? args.pop() : function () {}
+ const msg = {
+ destination: this.$parent.service.name,
+ path: this.$parent.name,
+ interface: this.$name,
+ member: mName,
+ }
+ if (this.$methods[mName] !== '') {
+ msg.signature = this.$methods[mName]
+ msg.body = args
+ }
+ bus.invoke(msg, callback)
+}
+
+DBusInterface.prototype.$createProp = function (propName, propType, propAccess) {
+ this.$properties[propName] = { type: propType, access: propAccess }
+ Object.defineProperty(this, propName, {
+ enumerable: true,
+ get: () => callback => this.$readProp(propName, callback), // eslint-disable-line unicorn/consistent-function-scoping
+ set(val) {
+ this.$writeProp(propName, val)
+ },
+ })
+}
+
+DBusInterface.prototype.$readProp = function (propName, callback) {
+ const bus = this.$parent.service.bus
+ bus.invoke(
+ {
+ destination: this.$parent.service.name,
+ path: this.$parent.name,
+ interface: 'org.freedesktop.DBus.Properties',
+ member: 'Get',
+ signature: 'ss',
+ body: [this.$name, propName],
+ },
+ (err, val) => {
+ if (err) {
+ callback(err)
+ } else {
+ const signature = val[0]
+ if (signature.length === 1) {
+ callback(err, val[1][0])
+ } else {
+ callback(err, val[1])
+ }
+ }
+ },
+ )
+}
+
+DBusInterface.prototype.$writeProp = function (propName, val) {
+ const bus = this.$parent.service.bus
+ bus.invoke({
+ destination: this.$parent.service.name,
+ path: this.$parent.name,
+ interface: 'org.freedesktop.DBus.Properties',
+ member: 'Set',
+ signature: 'ssv',
+ body: [this.$name, propName, [this.$properties[propName].type, val]],
+ })
+}
+
+function getMatchRule(objName, ifName, signame) {
+ return `type='signal',path='${objName}',interface='${ifName}',member='${signame}'`
+}
diff --git a/src/lib/dbus/marshall.ts b/src/lib/dbus/marshall.ts
new file mode 100644
index 000000000..5e4780570
--- /dev/null
+++ b/src/lib/dbus/marshall.ts
@@ -0,0 +1,110 @@
+// @ts-nocheck
+import assert from 'node:assert'
+import { Buffer } from 'node:buffer'
+
+import align from './align.js'
+import MakeSimpleMarshaller from './marshallers.js'
+import put from './put.js'
+import parseSignature from './signature.js'
+
+export default function marshall(signature, data, offset = 0) {
+ const tree = parseSignature(signature)
+ if (!Array.isArray(data) || data.length !== tree.length) {
+ throw new Error(
+ `message body does not match message signature. Body:${JSON.stringify(
+ data,
+ )}, signature:${signature}`,
+ )
+ }
+ const putstream = put()
+ putstream._offset = offset
+ const buf = writeStruct(putstream, tree, data).buffer()
+ return buf
+}
+
+function writeStruct(ps, tree, data) {
+ if (tree.length !== data.length) {
+ throw new Error('Invalid struct data')
+ }
+ for (let i = 0; i < tree.length; ++i) {
+ write(ps, tree[i], data[i])
+ }
+ return ps
+}
+
+function write(ps, ele, data) {
+ switch (ele.type) {
+ case '(':
+ case '{':
+ align(ps, 8)
+ writeStruct(ps, ele.child, data)
+ break
+ case 'a': {
+ // array serialisation:
+ // length of array body aligned at 4 byte boundary
+ // (optional 4 bytes to align first body element on 8-byte boundary if element
+ // body
+ const arrPut = put()
+ arrPut._offset = ps._offset
+ const _offset = arrPut._offset
+ writeSimple(arrPut, 'u', 0) // array length placeholder
+ const lengthOffset = arrPut._offset - 4 - _offset
+ // we need to align here because alignment is not included in array length
+ if (['x', 't', 'd', '{', '('].includes(ele.child[0].type)) {
+ align(arrPut, 8)
+ }
+ const startOffset = arrPut._offset
+ for (let i = 0; i < data.length; ++i) {
+ write(arrPut, ele.child[0], data[i])
+ }
+ const arrBuff = arrPut.buffer()
+ const length = arrPut._offset - startOffset
+ // lengthOffset in the range 0 to 3 depending on number of align bytes padded _before_ arrayLength
+ arrBuff.writeUInt32LE(length, lengthOffset)
+ ps.put(arrBuff)
+ ps._offset += arrBuff.length
+ break
+ }
+ case 'v': {
+ // TODO: allow serialisation of simple types as variants, e. g 123 -> ['u', 123], true -> ['b', 1], 'abc' -> ['s', 'abc']
+ assert.equal(data.length, 2, 'variant data should be [signature, data]')
+ const signatureEle = {
+ type: 'g',
+ child: [],
+ }
+ write(ps, signatureEle, data[0])
+ const tree = parseSignature(data[0])
+ assert(tree.length === 1)
+ write(ps, tree[0], data[1])
+ break
+ }
+ default:
+ return writeSimple(ps, ele.type, data)
+ }
+}
+
+const stringTypes = ['g', 'o', 's']
+
+function writeSimple(ps, type, data) {
+ if (typeof data === 'undefined') {
+ throw new TypeError('Serialisation of JS \'undefined\' type is not supported by d-bus')
+ }
+ if (data === null) {
+ throw new Error('Serialisation of null value is not supported by d-bus')
+ }
+
+ if (Buffer.isBuffer(data)) {
+ data = data.toString()// encoding?
+ }
+ if (stringTypes.includes(type) && typeof data !== 'string') {
+ throw new Error(
+ `Expected string or buffer argument, got ${JSON.stringify(
+ data,
+ )} of type '${type}'`,
+ )
+ }
+
+ const simpleMarshaller = MakeSimpleMarshaller(type)
+ simpleMarshaller.marshall(ps, data)
+ return ps
+}
diff --git a/src/lib/dbus/marshallers.ts b/src/lib/dbus/marshallers.ts
new file mode 100644
index 000000000..79f3aca0a
--- /dev/null
+++ b/src/lib/dbus/marshallers.ts
@@ -0,0 +1,341 @@
+// @ts-nocheck
+import Long from 'long'
+import { Buffer } from 'safe-buffer'
+
+import align from './align.js'
+import parseSignature from './signature.js'
+
+function checkRange(minValue, maxValue, data) {
+ if (data > maxValue || data < minValue) {
+ throw new Error('Number outside range')
+ }
+}
+
+function checkInteger(data) {
+ if (typeof data !== 'number') {
+ throw new TypeError(`Data: ${data} was not of type number`)
+ }
+ if (Math.floor(data) !== data) {
+ throw new Error(`Data: ${data} was not an integer`)
+ }
+}
+
+function checkBoolean(data) {
+ if (!(typeof data === 'boolean' || data === 0 || data === 1)) {
+ throw new Error(`Data: ${data} was not of type boolean`)
+ }
+}
+
+// This is essentially a tweaked version of 'fromValue' from Long.js with error checking.
+// This can take number or string of decimal characters or 'Long' instance (or Long-style object with props low,high,unsigned).
+function makeLong(val, signed) {
+ if (val instanceof Long) {
+ return val
+ }
+ if (val instanceof Number) {
+ val = val.valueOf()
+ }
+ if (typeof val === 'number') {
+ try {
+ // Long.js won't alert you to precision loss in passing more than 53 bit int through a double number, so we check here
+ checkInteger(val)
+ if (signed) {
+ checkRange(-0x1FFFFFFFFFFFFF, 0x1FFFFFFFFFFFFF, val)
+ } else {
+ checkRange(0, 0x1FFFFFFFFFFFFF, val)
+ }
+ } catch (e) {
+ e.message += ' (Number type can only carry 53 bit integer)'
+ throw e
+ }
+ try {
+ return Long.fromNumber(val, !signed)
+ } catch (e) {
+ e.message = `Error converting number to 64bit integer "${e.message}"`
+ throw e
+ }
+ }
+ if (typeof val === 'string' || val instanceof String) {
+ let radix = 10
+ val = val.trim().toUpperCase() // remove extra whitespace and make uppercase (for hex)
+ if (val.substring(0, 2) === '0X') {
+ radix = 16
+ val = val.substring(2)
+ } else if (val.substring(0, 3) === '-0X') {
+ // unusual, but just in case?
+ radix = 16
+ val = `-${val.substring(3)}`
+ }
+ val = val.replace(/^0+(?=\d)/, '') // dump leading zeroes
+ let data
+ try {
+ data = Long.fromString(val, !signed, radix)
+ } catch (e) {
+ e.message = `Error converting string to 64bit integer '${e.message}'`
+ throw e
+ }
+ // If string represents a number outside 64 bit range, it can quietly overflow.
+ // We assume if things converted correctly the string coming out of Long should match what went into it.
+ if (data.toString(radix).toUpperCase() !== val) {
+ throw new Error(
+ `Data: '${val}' did not convert correctly to ${
+ signed ? 'signed' : 'unsigned'
+ } 64 bit`,
+ )
+ }
+ return data
+ }
+ // Throws for non-objects, converts non-instanceof Long:
+ try {
+ return Long.fromBits(val.low, val.high, val.unsigned)
+ } catch (e) {
+ e.message = `Error converting object to 64bit integer '${e.message}'`
+ throw e
+ }
+}
+
+function checkLong(data, signed) {
+ if (!Long.isLong(data)) {
+ data = makeLong(data, signed)
+ }
+
+ // Do we enforce that Long.js object unsigned/signed match the field even if it is still in range?
+ // Probably, might help users avoid unintended bugs?
+ if (signed) {
+ if (data.unsigned) {
+ throw new Error(
+ 'Longjs object is unsigned, but marshalling into signed 64 bit field',
+ )
+ }
+ if (data.gt(Long.MAX_VALUE) || data.lt(Long.MIN_VALUE)) {
+ throw new Error(`Data: ${data} was out of range (64-bit signed)`)
+ }
+ } else {
+ if (!data.unsigned) {
+ throw new Error(
+ 'Longjs object is signed, but marshalling into unsigned 64 bit field',
+ )
+ }
+ // NOTE: data.gt(Long.MAX_UNSIGNED_VALUE) will catch if Long.js object is a signed value but is still within unsigned range!
+ // Since we are enforcing signed type matching between Long.js object and field, this note should not matter.
+ if (data.gt(Long.MAX_UNSIGNED_VALUE) || data.lt(0)) {
+ throw new Error(`Data: ${data} was out of range (64-bit unsigned)`)
+ }
+ }
+ return data
+}
+
+function checkValidSignature(data) {
+ if (data.length > 0xFF) {
+ throw new Error(
+ `Data: ${data} is too long for signature type (${data.length} > 255)`,
+ )
+ }
+
+ let parenCount = 0
+ for (let ii = 0; ii < data.length; ++ii) {
+ if (parenCount > 32) {
+ throw new Error(
+ `Maximum container type nesting exceeded in signature type:${data}`,
+ )
+ }
+ switch (data[ii]) {
+ case '(':
+ ++parenCount
+ break
+ case ')':
+ --parenCount
+ break
+ default:
+ /* no-op */
+ break
+ }
+ }
+ parseSignature(data)
+}
+
+function checkValidString(data) {
+ if (typeof data !== 'string') {
+ throw new TypeError(`Data: ${data} was not of type string`)
+ } else if (data.includes('\0')) {
+ throw new Error('String contains null byte')
+ }
+}
+
+/**
+ * MakeSimpleMarshaller
+ * @param signature - the signature of the data you want to check
+ * @returns a simple marshaller with the "check" method
+ *
+ * check returns nothing - it only raises errors if the data is
+ * invalid for the signature
+ */
+export default function MakeSimpleMarshaller(signature) {
+ const marshaller = {}
+
+ switch (signature) {
+ case 'o':
+ // object path
+ // TODO: verify object path here?
+ case 's': // eslint-disable-line no-fallthrough
+ // STRING
+ marshaller.check = function (data) {
+ checkValidString(data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ // utf8 string
+ align(ps, 4)
+ const buff = Buffer.from(data, 'utf8')
+ ps.word32le(buff.length).put(buff).word8(0)
+ ps._offset += 5 + buff.length
+ }
+ break
+ case 'g':
+ // SIGNATURE
+ marshaller.check = function (data) {
+ checkValidString(data)
+ checkValidSignature(data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ // signature
+ const buff = Buffer.from(data, 'ascii')
+ ps.word8(data.length).put(buff).word8(0)
+ ps._offset += 2 + buff.length
+ }
+ break
+ case 'y':
+ // BYTE
+ marshaller.check = function (data) {
+ checkInteger(data)
+ checkRange(0x00, 0xFF, data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ ps.word8(data)
+ ps._offset++
+ }
+ break
+ case 'b':
+ // BOOLEAN
+ marshaller.check = function (data) {
+ checkBoolean(data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ // booleans serialised as 0/1 unsigned 32 bit int
+ data = data ? 1 : 0
+ align(ps, 4)
+ ps.word32le(data)
+ ps._offset += 4
+ }
+ break
+ case 'n':
+ // INT16
+ marshaller.check = function (data) {
+ checkInteger(data)
+ checkRange(-0x7FFF - 1, 0x7FFF, data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ align(ps, 2)
+ const buff = Buffer.alloc(2)
+ buff.writeInt16LE(Number.parseInt(data), 0)
+ ps.put(buff)
+ ps._offset += 2
+ }
+ break
+ case 'q':
+ // UINT16
+ marshaller.check = function (data) {
+ checkInteger(data)
+ checkRange(0, 0xFFFF, data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ align(ps, 2)
+ ps.word16le(data)
+ ps._offset += 2
+ }
+ break
+ case 'i':
+ // INT32
+ marshaller.check = function (data) {
+ checkInteger(data)
+ checkRange(-0x7FFFFFFF - 1, 0x7FFFFFFF, data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ align(ps, 4)
+ const buff = Buffer.alloc(4)
+ buff.writeInt32LE(Number.parseInt(data), 0)
+ ps.put(buff)
+ ps._offset += 4
+ }
+ break
+ case 'u':
+ // UINT32
+ marshaller.check = function (data) {
+ checkInteger(data)
+ checkRange(0, 0xFFFFFFFF, data)
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ // 32 t unsigned int
+ align(ps, 4)
+ ps.word32le(data)
+ ps._offset += 4
+ }
+ break
+ case 't':
+ // UINT64
+ marshaller.check = function (data) {
+ return checkLong(data, false)
+ }
+ marshaller.marshall = function (ps, data) {
+ data = this.check(data)
+ align(ps, 8)
+ ps.word32le(data.low)
+ ps.word32le(data.high)
+ ps._offset += 8
+ }
+ break
+ case 'x':
+ // INT64
+ marshaller.check = function (data) {
+ return checkLong(data, true)
+ }
+ marshaller.marshall = function (ps, data) {
+ data = this.check(data)
+ align(ps, 8)
+ ps.word32le(data.low)
+ ps.word32le(data.high)
+ ps._offset += 8
+ }
+ break
+ case 'd':
+ // DOUBLE
+ marshaller.check = function (data) {
+ if (typeof data !== 'number') {
+ throw new TypeError(`Data: ${data} was not of type number`)
+ } else if (Number.isNaN(data)) {
+ throw new TypeError(`Data: ${data} was not a number`)
+ } else if (!Number.isFinite(data)) {
+ throw new TypeError('Number outside range')
+ }
+ }
+ marshaller.marshall = function (ps, data) {
+ this.check(data)
+ align(ps, 8)
+ const buff = Buffer.alloc(8)
+ buff.writeDoubleLE(Number.parseFloat(data), 0)
+ ps.put(buff)
+ ps._offset += 8
+ }
+ break
+ default:
+ throw new Error(`Unknown data type format: ${signature}`)
+ }
+ return marshaller
+}
diff --git a/src/lib/dbus/message.ts b/src/lib/dbus/message.ts
new file mode 100644
index 000000000..9df188314
--- /dev/null
+++ b/src/lib/dbus/message.ts
@@ -0,0 +1,125 @@
+// @ts-nocheck
+import { Buffer } from 'safe-buffer'
+
+import constants from './constants.js'
+import DBusBuffer from './dbus-buffer.js'
+import marshall from './marshall.js'
+
+const headerSignature = [
+ {
+ type: 'a',
+ child: [
+ {
+ type: '(',
+ child: [
+ {
+ type: 'y',
+ child: [],
+ },
+ {
+ type: 'v',
+ child: [],
+ },
+ ],
+ },
+ ],
+ },
+]
+
+export function unmarshalMessages(stream, onMessage, opts) {
+ let state = 0 // 0: header, 1: fields + body
+ let header, fieldsAndBody
+ let fieldsLength, fieldsLengthPadded
+ let fieldsAndBodyLength = 0
+ let bodyLength = 0
+ stream.on('readable', () => {
+ while (1) {
+ if (state === 0) {
+ header = stream.read(16)
+ if (!header) {
+ break
+ }
+ state = 1
+
+ fieldsLength = header.readUInt32LE(12)
+ fieldsLengthPadded = ((fieldsLength + 7) >> 3) << 3
+ bodyLength = header.readUInt32LE(4)
+ fieldsAndBodyLength = fieldsLengthPadded + bodyLength
+ } else {
+ fieldsAndBody = stream.read(fieldsAndBodyLength)
+ if (!fieldsAndBody) {
+ break
+ }
+ state = 0
+
+ const messageBuffer = new DBusBuffer(fieldsAndBody, undefined, opts)
+ const unmarshalledHeader = messageBuffer.readArray(
+ headerSignature[0].child[0],
+ fieldsLength,
+ )
+ messageBuffer.align(3)
+ let headerName
+ const message = {}
+ message.serial = header.readUInt32LE(8)
+
+ for (let i = 0; i < unmarshalledHeader.length; ++i) {
+ headerName = constants.headerTypeName[unmarshalledHeader[i][0]]
+ message[headerName] = unmarshalledHeader[i][1][1][0]
+ }
+
+ message.type = header[1]
+ message.flags = header[2]
+
+ if (bodyLength > 0 && message.signature) {
+ message.body = messageBuffer.read(message.signature)
+ }
+ onMessage(message)
+ }
+ }
+ })
+}
+
+export function marshalMessage(message) {
+ if (!message.serial) {
+ throw new Error('Missing or invalid serial')
+ }
+ const flags = message.flags || 0
+ const type = message.type || constants.messageType.methodCall
+ let bodyLength = 0
+ let bodyBuff
+ if (message.signature && message.body) {
+ bodyBuff = marshall(message.signature, message.body)
+ bodyLength = bodyBuff.length
+ }
+ const header = [
+ constants.endianness.le,
+ type,
+ flags,
+ constants.protocolVersion,
+ bodyLength,
+ message.serial,
+ ]
+ const headerBuff = marshall('yyyyuu', header)
+ const fields = []
+ constants.headerTypeName.forEach((fieldName) => {
+ const fieldVal = message[fieldName]
+ if (fieldVal) {
+ fields.push([
+ constants.headerTypeId[fieldName],
+ [constants.fieldSignature[fieldName], fieldVal],
+ ])
+ }
+ })
+ const fieldsBuff = marshall('a(yv)', [fields], 12)
+ const headerLenAligned
+ = ((headerBuff.length + fieldsBuff.length + 7) >> 3) << 3
+ const messageLen = headerLenAligned + bodyLength
+ const messageBuff = Buffer.alloc(messageLen)
+ headerBuff.copy(messageBuff)
+ fieldsBuff.copy(messageBuff, headerBuff.length)
+ if (bodyLength > 0) {
+ bodyBuff.copy(messageBuff, headerLenAligned)
+ }
+
+ return messageBuff
+}
diff --git a/src/lib/dbus/put.ts b/src/lib/dbus/put.ts
new file mode 100644
index 000000000..59f36db37
--- /dev/null
+++ b/src/lib/dbus/put.ts
@@ -0,0 +1,94 @@
+// @ts-nocheck
+import assert from 'node:assert'
+import { Buffer } from 'node:buffer'
+
+export default function Put() {
+ if (!(this instanceof Put)) {
+ return new Put()
+ }
+
+ const words = []
+ let len = 0
+
+ this.put = function (buf) {
+ words.push({ buffer: buf })
+ len += buf.length
+ return this
+ }
+
+ this.word8 = function (x) {
+ words.push({ bytes: 1, value: x })
+ len += 1
+ return this
+ }
+
+ [8, 16, 24, 32, 64].forEach((bits) => {
+ this[`word${bits}be`] = function (x) {
+ words.push({ endian: 'big', bytes: bits / 8, value: x })
+ len += bits / 8
+ return this
+ }
+
+ this[`word${bits}le`] = function (x) {
+ words.push({ endian: 'little', bytes: bits / 8, value: x })
+ len += bits / 8
+ return this
+ }
+ })
+
+ this.pad = function (bytes) {
+ assert(Number.isInteger(bytes), 'pad(bytes) must be supplied with an integer!')
+ words.push({ endian: 'big', bytes, value: 0 })
+ len += bytes
+ return this
+ }
+
+ this.length = function () {
+ return len
+ }
+
+ this.buffer = function () {
+ const buf = Buffer.alloc(len)
+ let offset = 0
+ words.forEach((word) => {
+ if (word.buffer) {
+ word.buffer.copy(buf, offset, 0)
+ offset += word.buffer.length
+ } else if (word.bytes === 'float') {
+ // s * f * 2^e
+ const v = Math.abs(word.value)
+ const s = (word.value >= 0) * 1
+ const e = Math.ceil(Math.log(v) / Math.LN2)
+ const f = v / (1 << e)
+
+ // s:1, e:7, f:23
+ // [seeeeeee][efffffff][ffffffff][ffffffff]
+ buf[offset++] = (s << 7) & ~~(e / 2)
+ buf[offset++] = ((e & 1) << 7) & ~~(f / (1 << 16))
+ buf[offset++] = 0
+ buf[offset++] = 0
+ offset += 4
+ } else {
+ const big = word.endian === 'big'
+ const ix = big ? [(word.bytes - 1) * 8, -8] : [0, 8]
+
+ for (
+ let i = ix[0];
+ big ? i >= 0 : i < word.bytes * 8;
+ i += ix[1]
+ ) {
+ if (i >= 32) {
+ buf[offset++] = Math.floor(word.value / 2 ** i) & 0xFF
+ } else {
+ buf[offset++] = (word.value >> i) & 0xFF
+ }
+ }
+ }
+ })
+ return buf
+ }
+
+ this.write = function (stream) {
+ stream.write(this.buffer())
+ }
+}
diff --git a/src/lib/dbus/readline.ts b/src/lib/dbus/readline.ts
new file mode 100644
index 000000000..36c7a71e6
--- /dev/null
+++ b/src/lib/dbus/readline.ts
@@ -0,0 +1,26 @@
+// @ts-nocheck
+import { Buffer } from 'safe-buffer'
+
+export default function readOneLine(stream, cb) {
+ const bytes: any[] = []
+ function readable() {
+ while (1) {
+ const buf = stream.read(1)
+ if (!buf) {
+ return
+ }
+ const b = buf[0]
+ if (b === 0x0A) {
+ try {
+ cb(Buffer.from(bytes))
+ } catch (error) {
+ stream.emit('error', error)
+ }
+ stream.removeListener('readable', readable)
+ return
+ }
+ bytes.push(b)
+ }
+ }
+ stream.on('readable', readable)
+}
diff --git a/src/lib/dbus/signature.ts b/src/lib/dbus/signature.ts
new file mode 100644
index 000000000..6cd8b3649
--- /dev/null
+++ b/src/lib/dbus/signature.ts
@@ -0,0 +1,63 @@
+// @ts-nocheck
+const match = {
+ '{': '}',
+ '(': ')',
+}
+
+const knownTypes = {}
+'(){}ybnqiuxtdsogarvehm*?@&^'.split('').forEach((c) => {
+ knownTypes[c] = true
+})
+
+function checkNotEnd(c) {
+ if (!c) {
+ throw new Error('Bad signature: unexpected end')
+ }
+ return c
+}
+
+export default function parseSignature(signature) {
+ let index = 0
+ function next() {
+ if (index < signature.length) {
+ const c = signature[index]
+ ++index
+ return c
+ }
+ return null
+ }
+
+ function parseOne(c) {
+ if (!knownTypes[c]) {
+ throw new Error(`Unknown type: "${c}" in signature "${signature}"`)
+ }
+
+ let ele
+ const res = { type: c, child: [] }
+ switch (c) {
+ case 'a': // array
+ ele = next()
+ checkNotEnd(ele)
+ res.child.push(parseOne(ele))
+ return res
+ case '{': // dict entry
+ case '(': // struct
+ ele = next()
+ while (ele !== null && ele !== match[c]) {
+ res.child.push(parseOne(ele))
+ ele = next()
+ }
+ checkNotEnd(ele)
+ return res
+ }
+ return res
+ }
+
+ const ret = []
+ let c = next()
+ while (c !== null) {
+ ret.push(parseOne(c))
+ c = next()
+ }
+ return ret
+}
diff --git a/src/lib/dbus/stdifaces.ts b/src/lib/dbus/stdifaces.ts
new file mode 100644
index 000000000..52673fc39
--- /dev/null
+++ b/src/lib/dbus/stdifaces.ts
@@ -0,0 +1,212 @@
+// @ts-nocheck
+import constants from './constants.js'
+import parseSignature from './signature.js'
+
+// TODO: use xmlbuilder
+
+const xmlHeader
+ = ''
+let stdIfaces
+
+export default function (msg, bus) {
+ if (
+ msg.interface === 'org.freedesktop.DBus.Introspectable'
+ && msg.member === 'Introspect'
+ ) {
+ if (msg.path === '/') {
+ msg.path = ''
+ }
+
+ const resultXml = [xmlHeader]
+ const nodes = {}
+ // TODO: this is not very efficient for large number of exported objects
+ // need to build objects tree as they are exported and walk this tree on introspect request
+ for (const path in bus.exportedObjects) {
+ if (path.indexOf(msg.path) === 0) {
+ // objects path starts with requested
+ const introspectableObj = bus.exportedObjects[msg.path]
+ if (introspectableObj) {
+ nodes[msg.path] = introspectableObj
+ } else {
+ if (path[msg.path.length] !== '/') {
+ continue
+ }
+ const localPath = path.slice(msg.path.length)
+ const pathParts = localPath.split('/')
+ const localName = pathParts[1]
+ nodes[localName] = null
+ }
+ }
+ }
+
+ const length = Object.keys(nodes).length
+ let obj
+ if (length === 0) {
+ resultXml.push('')
+ } else if (length === 1) {
+ obj = nodes[Object.keys(nodes)[0]]
+ if (obj) {
+ resultXml.push('')
+ for (const ifaceNode in obj) {
+ resultXml.push(interfaceToXML(obj[ifaceNode][0]))
+ }
+ resultXml.push(stdIfaces)
+ resultXml.push('')
+ } else {
+ resultXml.push(
+ `\n \n `,
+ )
+ }
+ } else {
+ resultXml.push('')
+ for (const name in nodes) {
+ if (nodes[name] === null) {
+ resultXml.push(` `)
+ } else {
+ obj = nodes[name]
+ resultXml.push(` `)
+ for (const ifaceName in obj) {
+ resultXml.push(interfaceToXML(obj[ifaceName][0]))
+ }
+ resultXml.push(stdIfaces)
+ resultXml.push(' ')
+ }
+ }
+ resultXml.push('')
+ }
+
+ const introspectableReply = {
+ type: constants.messageType.methodReturn,
+ serial: bus.serial++,
+ replySerial: msg.serial,
+ destination: msg.sender,
+ signature: 's',
+ body: [resultXml.join('\n')],
+ }
+ bus.connection.message(introspectableReply)
+ return 1
+ } else if (msg.interface === 'org.freedesktop.DBus.Properties') {
+ const interfaceName = msg.body[0]
+ const propertiesObj = bus.exportedObjects[msg.path]
+ // TODO: !propertiesObj -> UnknownObject http://www.freedesktop.org/wiki/Software/DBusBindingErrors
+ if (!propertiesObj || !propertiesObj[interfaceName]) {
+ // TODO:
+ bus.sendError(
+ msg,
+ 'org.freedesktop.DBus.Error.UnknownMethod',
+ 'Uh oh oh',
+ )
+ return 1
+ }
+ const impl = propertiesObj[interfaceName][1]
+
+ const propertiesReply = {
+ type: constants.messageType.methodReturn,
+ serial: bus.serial++,
+ replySerial: msg.serial,
+ destination: msg.sender,
+ }
+ if (msg.member === 'Get' || msg.member === 'Set') {
+ const propertyName = msg.body[1]
+ const propType = propertiesObj[interfaceName][0].properties[propertyName]
+ if (msg.member === 'Get') {
+ const propValue = impl[propertyName]
+ propertiesReply.signature = 'v'
+ propertiesReply.body = [[propType, propValue]]
+ } else {
+ impl[propertyName] = 1234 // TODO: read variant and set property value
+ }
+ } else if (msg.member === 'GetAll') {
+ propertiesReply.signature = 'a{sv}'
+ const props = []
+ for (const p in propertiesObj[interfaceName][0].properties) {
+ const propertySignature = propertiesObj[interfaceName][0].properties[p]
+ props.push([p, [propertySignature, impl[p]]])
+ }
+ propertiesReply.body = [props]
+ }
+ bus.connection.message(propertiesReply)
+ return 1
+ } else if (msg.interface === 'org.freedesktop.DBus.Peer') {
+ // TODO: implement bus.replyTo(srcMsg, signature, body) method
+ const peerReply = {
+ type: constants.messageType.methodReturn,
+ serial: bus.serial++,
+ replySerial: msg.serial,
+ destination: msg.sender,
+ }
+ if (msg.member === 'Ping') {
+ // empty body
+ } else if (msg.member === 'GetMachineId') {
+ peerReply.signature = 's'
+ peerReply.body = ['This is a machine id. TODO: implement']
+ }
+ bus.connection.message(peerReply)
+ return 1
+ }
+ return 0
+}
+
+function interfaceToXML(iface) {
+ const result = []
+ const dumpArgs = function (argsSignature, argsNames, direction) {
+ if (!argsSignature) {
+ return
+ }
+ const args = parseSignature(argsSignature)
+ args.forEach((arg, num) => {
+ const argName = argsNames ? argsNames[num] : direction + num
+ const dirStr = direction === 'signal' ? '' : `" direction="${direction}`
+ result.push(
+ ` `,
+ )
+ })
+ }
+ result.push(` `)
+ if (iface.methods) {
+ for (const methodName in iface.methods) {
+ const method = iface.methods[methodName]
+ result.push(` `)
+ dumpArgs(method[0], method[2], 'in')
+ dumpArgs(method[1], method[3], 'out')
+ result.push(' ')
+ }
+ }
+ if (iface.signals) {
+ for (const signalName in iface.signals) {
+ const signal = iface.signals[signalName]
+ result.push(` `)
+ dumpArgs(signal[0], signal.slice(1), 'signal')
+ result.push(' ')
+ }
+ }
+ if (iface.properties) {
+ for (const propertyName in iface.properties) {
+ // TODO: decide how to encode access
+ result.push(
+ ` `,
+ )
+ }
+ }
+ result.push(' ')
+ return result.join('\n')
+}
+
+function dumpSignature(s) {
+ const result = []
+ s.forEach((sig) => {
+ result.push(sig.type + dumpSignature(sig.child))
+ if (sig.type === '{') {
+ result.push('}')
+ }
+ if (sig.type === '(') {
+ result.push(')')
+ }
+ })
+ return result.join('')
+}
+stdIfaces
+ = ' \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n '