hm-simulator 0.1.1 → 1.1.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/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};