hm-simulator 0.1.1 → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +330 -67
- package/behaviors/virtual-button-press.js +1 -1
- package/behaviors/window-open-close.js +1 -1
- package/data/fixtures/devices.json +31606 -0
- package/data/fixtures/lab-devices.json +4563 -0
- package/index.js +72 -7
- package/lib/binrpc-codec.js +100 -0
- package/lib/faults.js +126 -0
- package/lib/method-help.js +50 -0
- package/lib/rega.js +245 -0
- package/lib/servers.js +150 -0
- package/lib/sim.js +1751 -0
- package/lib/tls.js +160 -0
- package/lib/validate.js +233 -0
- package/package.json +31 -22
- package/sim.js +3 -541
- package/sim.mjs +5 -0
- package/.travis.yml +0 -3
- package/config.js +0 -3
package/lib/servers.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {EventEmitter} = require('node:events');
|
|
4
|
+
|
|
5
|
+
const binrpc = require('binrpc');
|
|
6
|
+
const xmlrpc = require('homematic-xmlrpc');
|
|
7
|
+
|
|
8
|
+
const {encodeResponse, encodeFault} = require('./binrpc-codec.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* binrpc server.
|
|
12
|
+
*
|
|
13
|
+
* The dispatch is taken over from binrpc's own event based handling so that faults can be answered
|
|
14
|
+
* with message type 0xff. binrpc decodes the request for us, we encode the response.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {string} options.host
|
|
18
|
+
* @param {number} options.port
|
|
19
|
+
* @param {function} options.dispatch (method, params, callback) - callback(fault, result)
|
|
20
|
+
* @param {function} [options.onListening]
|
|
21
|
+
* @returns {object} the binrpc Server instance
|
|
22
|
+
*/
|
|
23
|
+
function createBinrpcServer({host, port, dispatch, onListening, onError}) {
|
|
24
|
+
const server = binrpc.createServer({host, port}, () => {
|
|
25
|
+
if (typeof onListening === 'function') {
|
|
26
|
+
onListening(server);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
server.handleCall = (request, socket) => {
|
|
31
|
+
const method = request && request.method;
|
|
32
|
+
const params = (request && request.params) || [];
|
|
33
|
+
if (typeof method !== 'string' || method === '') {
|
|
34
|
+
socket.write(encodeResponse(''));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dispatch(method, params, (fault, result) => {
|
|
39
|
+
socket.write(fault ? encodeFault(fault) : encodeResponse(result));
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
server.on('error', (error) => {
|
|
44
|
+
if (typeof onError === 'function') {
|
|
45
|
+
onError(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return server;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// events of the xmlrpc server that are not RPC method names
|
|
53
|
+
const INTERNAL_EVENTS = new Set(['NotFound', 'error', 'listening', 'newListener', 'removeListener']);
|
|
54
|
+
|
|
55
|
+
function unauthorized(response, realm) {
|
|
56
|
+
response.writeHead(401, {
|
|
57
|
+
'WWW-Authenticate': `Basic realm="${realm}"`,
|
|
58
|
+
'Content-Type': 'text/plain',
|
|
59
|
+
Connection: 'close',
|
|
60
|
+
});
|
|
61
|
+
response.end('Unauthorized');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* xmlrpc server, optionally with TLS and/or HTTP basic auth.
|
|
66
|
+
*
|
|
67
|
+
* homematic-xmlrpc dispatches per method name and answers method names without a listener with an
|
|
68
|
+
* empty string. To be able to answer unknown methods with a fault - and to avoid having to register
|
|
69
|
+
* a listener per method - `listenerCount` and `emit` are wrapped so that every method call reaches
|
|
70
|
+
* the simulator's dispatch.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} options
|
|
73
|
+
* @param {string} options.host
|
|
74
|
+
* @param {number} options.port
|
|
75
|
+
* @param {function} options.dispatch (method, params, callback) - callback(fault, result)
|
|
76
|
+
* @param {object} [options.tls] {key, cert} - enables https
|
|
77
|
+
* @param {object} [options.auth] {username, password, realm}
|
|
78
|
+
* @param {string} [options.path] if set, other paths are answered with 404
|
|
79
|
+
* @param {function} [options.onListening]
|
|
80
|
+
* @returns {object} the homematic-xmlrpc Server instance
|
|
81
|
+
*/
|
|
82
|
+
function createXmlrpcServer({host, port, dispatch, tls, auth, path, onListening, onError}) {
|
|
83
|
+
const serverOptions = tls ? {host, port, ...tls} : {host, port};
|
|
84
|
+
const listening = () => {
|
|
85
|
+
if (typeof onListening === 'function') {
|
|
86
|
+
onListening(server);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const server = tls
|
|
90
|
+
? xmlrpc.createSecureServer(serverOptions, listening)
|
|
91
|
+
: xmlrpc.createServer(serverOptions, listening);
|
|
92
|
+
|
|
93
|
+
server.listenerCount = function (eventName) {
|
|
94
|
+
return INTERNAL_EVENTS.has(eventName) ? EventEmitter.prototype.listenerCount.call(this, eventName) : 1;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const originalEmit = server.emit;
|
|
98
|
+
server.emit = function (eventName, ...args) {
|
|
99
|
+
if (!INTERNAL_EVENTS.has(eventName) && args.length === 3 && typeof args[2] === 'function') {
|
|
100
|
+
const [, params, callback] = args;
|
|
101
|
+
dispatch(eventName, params || [], (fault, result) => {
|
|
102
|
+
if (fault) {
|
|
103
|
+
callback({faultCode: fault.faultCode, faultString: fault.faultString});
|
|
104
|
+
} else {
|
|
105
|
+
callback(null, result === undefined ? '' : result);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return originalEmit.call(this, eventName, ...args);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
server.on('NotFound', () => {});
|
|
115
|
+
server.on('error', (error) => {
|
|
116
|
+
if (typeof onError === 'function') {
|
|
117
|
+
onError(error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (auth || path) {
|
|
122
|
+
const httpServer = server.httpServer;
|
|
123
|
+
const [handleMethodCall] = httpServer.listeners('request');
|
|
124
|
+
httpServer.removeAllListeners('request');
|
|
125
|
+
const expected = auth
|
|
126
|
+
? 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64')
|
|
127
|
+
: null;
|
|
128
|
+
httpServer.on('request', (request, response) => {
|
|
129
|
+
if (path) {
|
|
130
|
+
const requestPath = (request.url || '/').split('?')[0].replace(/\/+$/, '');
|
|
131
|
+
if (requestPath !== path.replace(/\/+$/, '') && requestPath !== '') {
|
|
132
|
+
response.writeHead(404, {'Content-Type': 'text/plain'});
|
|
133
|
+
response.end('Not Found');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (expected && request.headers.authorization !== expected) {
|
|
139
|
+
unauthorized(response, auth.realm || 'hm-simulator');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
handleMethodCall(request, response);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return server;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {createBinrpcServer, createXmlrpcServer};
|