dcp-worker 4.1.0 → 4.2.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file remote-console.js
2
+ * @file telnetd.js
3
3
  * DCP Service Worker support for a remote console, accessible via telnet.
4
4
  *
5
5
  * * SECURITY NOTICE *
@@ -23,9 +23,10 @@
23
23
  */
24
24
  'use strict';
25
25
 
26
- const path = require('path');
27
- const fs = require('fs');
28
- const { debugging } = require('./utils');
26
+ const path = require('path');
27
+ const fs = require('fs');
28
+ const debug = require('debug');
29
+
29
30
  var dcpConfig;
30
31
  var mainEval;
31
32
  var ci;
@@ -46,10 +47,29 @@ function daemonEval()
46
47
  return eval(arguments[0]); /* eslint-disable-line no-eval */
47
48
  }
48
49
 
49
- function callbackTelnet(port, client, registry)
50
+ function callbackTelnet()
50
51
  {
51
- client.unref();
52
- debugging() && console.notice(' ! telnetd - listening on port', port);
52
+ /* The telnet daemon is purposefully started as early and synchronously as possible, so that it is
53
+ * ready to capture errors in and/or help troubleshoot the most basic tasks, like setting up the
54
+ * console, log multiplexing, or command-line option parsing.
55
+ *
56
+ * In this callback, we purposefully push the telnetd startup status reporting so that it is more
57
+ * likely that the multiplexed loggers and/or the dashboard will be ready for the diagnostic
58
+ * messages.
59
+ */
60
+ if (arguments.length === 1)
61
+ {
62
+ const [ error ] = arguments;
63
+ setTimeout(() => console.error(` ! telnetd - startup error ${error.code || error.message}`), 250);
64
+ debug('dcp-worker:telnet')('telnet listen error:', error);
65
+ }
66
+ else
67
+ {
68
+ const [ port, server, _registry ] = arguments; // eslint-disable-line no-unused-vars
69
+ setTimeout(() => console.warn(' ! telnetd listening on port', port), 250);
70
+ debug('dcp-worker:telnet')('telnetd listening on', server.address().family, server.address().address, server.address().port);
71
+ server.unref();
72
+ }
53
73
  }
54
74
 
55
75
  /**
@@ -64,7 +84,7 @@ exports.setMainEval = function removeConsole$$setMainEval()
64
84
  /**
65
85
  * Initialize the remote console
66
86
  *
67
- * @param {...object} commands Command definitions. See telnet-console documentation
87
+ * @param {...object} commands Command definitions. See telnet-console documentation
68
88
  * for details
69
89
  */
70
90
  exports.init = function remoteConsole$$init(...commands)
@@ -84,16 +104,8 @@ exports.init = function remoteConsole$$init(...commands)
84
104
 
85
105
  console.warn('*** Enabling telnet daemon on port', port, '(security risk) ***');
86
106
 
87
- if (port !== 0)
88
- exports.port = port;
89
- else
90
- {
91
- /* telnet-console library does not properly support port 0 so we mostly work-around here */
92
- exports.port = Math.floor(1023 + (Math.random() * (63 * 1024)));
93
- }
94
-
95
107
  ci = require('telnet-console').start({
96
- port: exports.port,
108
+ port,
97
109
  callbackTelnet,
98
110
  eval: daemonEval,
99
111
  histfile: edcFilename + '.history',
@@ -105,12 +117,13 @@ exports.init = function remoteConsole$$init(...commands)
105
117
  }
106
118
  catch(e)
107
119
  {
108
- console.warn(' ! Failed to enable telnet daemon:', e.message);
120
+ console.warn(` ! Failed to enable telnet daemon (${e.code || e.message})`);
121
+ debug('dcp-worker:telnet')('telnet daemon error:', e);
109
122
  }
110
123
  }
111
124
 
112
125
  /**
113
- * Re-intercept log calls. To be used after a logger may have added its own override.
126
+ * Re-intercept log calls. To be used after the globalThis.console has been replaced or mutated.
114
127
  */
115
128
  exports.reintercept = function remoteConsole$$reintercept()
116
129
  {
package/lib/utils.js CHANGED
@@ -1,14 +1,21 @@
1
-
2
1
  /**
3
2
  * @file utils.js
4
3
  * Shared library code.
5
4
  *
6
5
  * @author Paul, paul@distributive.network
7
6
  * @date August 2023
7
+ * @author Wes Garland, wes@distributive.network
8
+ * @date June 2025
8
9
  */
9
10
  'use strict';
10
11
 
11
- const process = require('process');
12
+ exports.slicesFetched = slicesFetched;
13
+ exports.debugging = debugging;
14
+ exports.isHash = isHash;
15
+ exports.shortLoc = shortLoc;
16
+ exports.qty = qty;
17
+ exports.uniq = uniq;
18
+ exports.makeSyslogUrl = makeSyslogUrl;
12
19
 
13
20
  /**
14
21
  * Figure out #slices fetched from the different forms of the 'fetch' event.
@@ -33,14 +40,110 @@ function debugging()
33
40
  }
34
41
 
35
42
  /**
36
- * Flag to display detailed debug info in diagnostics.
37
- * @return {boolean}
43
+ * Output hostname:port for an arbitrary URL object
44
+ * @param {URL|DcpURL} url
45
+ */
46
+ function shortLoc(url)
47
+ {
48
+ var port = url.port;
49
+
50
+ if (!port)
51
+ {
52
+ switch(url.protocol)
53
+ {
54
+ case 'http:':
55
+ case 'ws:':
56
+ port = 80;
57
+ break;
58
+ case 'https:':
59
+ case 'wss:':
60
+ port = 443;
61
+ break;
62
+ case 'ftp:':
63
+ port = 21;
64
+ break;
65
+ }
66
+ }
67
+
68
+ return `${url.hostname}:${port}`;
69
+ }
70
+
71
+ /**
72
+ * Imperfect, but handles CG { joinKey, joinHash }.
73
+ * @param {string} b string which might be a join hash
74
+ * @returns {boolean} returns true when b is a join hash; false otherwise
38
75
  */
39
- function displayMaxDiagInfo ()
76
+ function isHash(b)
40
77
  {
41
- return Boolean(process.env.DCP_SUPERVISOR_DEBUG_DISPLAY_MAX_INFO) || debugging() || false;
78
+ return b && b.length === 68 && b.startsWith('eh1-');
42
79
  }
43
80
 
44
- exports.slicesFetched = slicesFetched;
45
- exports.debugging = debugging;
46
- exports.displayMaxDiagInfo = displayMaxDiagInfo;
81
+ /**
82
+ * Display a quantity of things with units
83
+ * @param {number} amount the number of things
84
+ * @param {string} singular singular form of unit
85
+ * @param {string} plural plural form of unit
86
+ * @returns {string}
87
+ * @example
88
+ * qty(1, 'apple', 'apples') => '1 apple'
89
+ * qty(2, 'apple', 'apples') => '2 apples'
90
+ * @todo i18n
91
+ */
92
+ function qty(amount, singular, plural)
93
+ {
94
+ if (Array.isArray(amount))
95
+ amount = amount.length;
96
+ if (!plural)
97
+ plural = singular + 's';
98
+ if (!amount)
99
+ return plural;
100
+ if (Number(amount) === 1)
101
+ return singular;
102
+ return plural;
103
+ }
104
+
105
+ /**
106
+ * Return a new array composed only of unique elements of arr
107
+ * @param {Array} arr
108
+ * @returns {Array}
109
+ */
110
+ function uniq(array)
111
+ {
112
+ return array.filter((val, idx, arr) => arr.indexOf(val) === idx);
113
+ }
114
+
115
+ /**
116
+ * Syslog URLs are bit special - the pathname of the URL encodes the syslog facility. Additionally, a
117
+ * plain string treated as just a pathname and no other components
118
+ *
119
+ * existingUrl newArg result
120
+ * ------------------------------- -------------------------------- ----------------------------------
121
+ * udp://localhost:513/local0 tls://provider.com:123/local1 tls://provider.com:123/local1
122
+ * udp://localhost:513/local0 local1 udp://localhost:513/local1
123
+ * udp://localhost:513/local0 tls://provider.com:123/ tls://provider.com:123/local0
124
+ *
125
+ * @param {URL} existingUrl
126
+ * @param {string|URL} newArg
127
+ * @returns {URL}
128
+ */
129
+ function makeSyslogUrl(existingUrl, newArg)
130
+ {
131
+ var newUrl;
132
+
133
+ if (/^[A-Za-z]+[0-9]?$/.test(newArg))
134
+ {
135
+ newUrl = new URL(existingUrl.href);
136
+ newUrl.pathname = newArg;
137
+ }
138
+ else
139
+ {
140
+ newUrl = new URL(newArg)
141
+ if (!newUrl.pathname)
142
+ newUrl.pathname = existingUrl.pathname;
143
+ }
144
+
145
+ if (!newUrl.pathname)
146
+ newUrl.pathname = 'local7';
147
+
148
+ return newUrl;
149
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * https://stackoverflow.com/questions/52843900/blessed-server-node-js-over-websocket-to-xterm-js-client-in-browser
3
+ */
4
+ const blessed = require('blessed');
5
+ const contrib = require('blessed-contrib');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const http = require('http');
9
+
10
+ const progDir = path.dirname(require.main.filename);
11
+ const wwwDir = path.resolve(progDir, '../www');
12
+ const nmDir = path.resolve(progDir, '../node_modules');
13
+
14
+ const magic = {
15
+ html: 'text/html',
16
+ txt: 'text/plain',
17
+ js: 'application/javascript',
18
+ css: 'text/css',
19
+ gif: 'image/gif',
20
+ jpeg: 'image/jpeg',
21
+ jpg: 'image/jpeg',
22
+ png: 'image/png',
23
+ svg: 'image/svg+xml',
24
+ kvin: 'application/x-kvin',
25
+ }
26
+
27
+ const clients = [ ];
28
+ var worker; /* instance of DistributiveWorker, null (stopped), or undefined (not started) */
29
+
30
+ /**
31
+ * Return a copy of the local dcp config, with the current worker's config at dcpConfig.worker, but with
32
+ * with sensitive information (eg compute group credentials) stripped.
33
+ */
34
+ function makeSafeConfig()
35
+ {
36
+ const conf = Object.assign({}, dcpConfig);
37
+ conf.worker = Object.assign({}, worker.config || dcpConfig.worker?.config);
38
+
39
+ /* Sanitize worker.computeGroups credentials */
40
+ if (conf.worker?.computeGroups)
41
+ {
42
+ conf.worker.computeGroups = Object.assign({}, conf.worker.computeGroups);
43
+ for (let key in conf.worker.computeGroups)
44
+ {
45
+ const group = conf.worker.computeGroups[key];
46
+ if (group.joinKey)
47
+ group = { joinKey: group.joinKey };
48
+ else
49
+ group = {};
50
+ conf.worker.computeGroups[key] = group;
51
+ }
52
+ }
53
+
54
+ return conf;
55
+ }
56
+
57
+ /**
58
+ * HTTP server
59
+ */
60
+ function handleHttpRequest(request, response)
61
+ {
62
+ if (request.url === '/etc/dcp-config.js')
63
+ {
64
+ response.setHeader('Content-Type', magic.js);
65
+ response.end(makeSkeletonConfig());
66
+ return;
67
+ }
68
+
69
+ if (request.url === '/etc/dcp-config.kvin')
70
+ {
71
+ response.setHeader('Content-Type', magic.kvin);
72
+ response.end('(' + JSON.stringify(dcpConfig) + ')');
73
+ return;
74
+ }
75
+
76
+ var filename = (request.url.slice(1) || 'index.html');
77
+ const contentType = magic[path.extname(filename).slice(1)] || 'text/plain';
78
+ var errorStatus, contentDir;
79
+
80
+ if (!filename.startsWith('node_modules/'))
81
+ contentDir = wwwDir;
82
+ else
83
+ {
84
+ contentDir = nmDir;
85
+ filename = filename.slice(13);
86
+ }
87
+
88
+ filename = path.resolve(contentDir, filename);
89
+ console.log({contentDir,filename,requestUrl:request.url});
90
+ if (!filename.startsWith(contentDir) || filename[0] !== '/')
91
+ errorStatus = 401;
92
+ else if (!fs.existsSync(filename))
93
+ errorStatus = 404;
94
+
95
+ if (errorStatus)
96
+ {
97
+ response.setHeader('Content-Type', 'text/plain');
98
+ response.statusCode = errorStatus;
99
+ response.end(`${errorStatus} accessing ${filename}`);
100
+ return;
101
+ }
102
+
103
+ response.setHeader('Content-Type', contentType);
104
+ response.end(fs.readFileSync(filename));
105
+ }
106
+
107
+ exports.getConfig = function webConsole$$getConfig()
108
+ {
109
+ var config = { host: 'localhost', port: 9080 };
110
+
111
+ if (dcpConfig.worker?.webConsole?.listen)
112
+ {
113
+ config.host = dcpConfig.worker.webConsole.listen.host;
114
+ config.port = dcpConfig.worker.webConsole.listen.port || 80;
115
+ }
116
+
117
+ return config;
118
+ }
119
+
120
+ exports.a$init = function webConsole$$init()
121
+ {
122
+ var server = http.createServer(handleHttpRequest).unref();
123
+ var resolve, reject;
124
+ var a$Promise = new Promise((_resolve, _reject) => {
125
+ resolve = _resolve;
126
+ reject = _reject;
127
+ });
128
+
129
+ server.on('error', error => {
130
+ console.error(` ! Web console not started; ${error.message}`);
131
+ reject(error);
132
+ });
133
+ server.listen(exports.getConfig(), function webConsole$$ready() {
134
+ resolve();
135
+ console.log(` * httpd listening on ${server._connectionKey}`);
136
+ });
137
+
138
+ /**
139
+ * WebSocket server
140
+ */
141
+ const WebSocketServer = require('websocket').server;
142
+ const wsServer = new WebSocketServer({
143
+ httpServer: server,
144
+ autoAcceptConnections: false
145
+ });
146
+
147
+ wsServer.on('request', handleWssRequest);
148
+ function handleWssRequest(request)
149
+ {
150
+ const connection = request.accept(null, request.origin);
151
+ connection.socket.unref();
152
+ clients.push(connection);
153
+
154
+ const write = connection.send;
155
+ const read = connection.socket.read;
156
+
157
+ connection.send(JSON.stringify({ type: 'log', facility: 'debug', message: 'hello, world' }));
158
+ connection.on('message', messageHandler);
159
+ connection.on('close', closeHandler);
160
+
161
+ function messageHandler(message)
162
+ {
163
+ console.log('message:', message)
164
+ console.log('this:', this);
165
+ }
166
+
167
+ function closeHandler()
168
+ {
169
+ }
170
+ }
171
+
172
+ return a$Promise;
173
+ }
174
+
175
+ exports.setWorker = function webConsole$$setWorker(dw)
176
+ {
177
+ worker = dw;
178
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * https://stackoverflow.com/questions/52843900/blessed-server-node-js-over-websocket-to-xterm-js-client-in-browser
3
+ */
4
+ const blessed = require('blessed');
5
+ const contrib = require('blessed-contrib');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const http = require('http');
9
+
10
+ const progDir = path.dirname(require.main.filename);
11
+ const wwwDir = path.resolve(progDir, '../www');
12
+ const nmDir = path.resolve(progDir, '../node_modules');
13
+ const listenHost = listenUrl.hostname === 'localhost' ? '::' : listenUrl.hostname;
14
+
15
+ const magic = {
16
+ html: 'text/html',
17
+ txt: 'text/plain',
18
+ js: 'application/javascript',
19
+ css: 'text/css',
20
+ gif: 'image/gif',
21
+ jpeg: 'image/jpeg',
22
+ jpg: 'image/jpeg',
23
+ png: 'image/png',
24
+ svg: 'image/svg+xml',
25
+ kvin: 'application/x-kvin',
26
+ }
27
+
28
+ const clients = [ ];
29
+
30
+ function makeSkeletonConfig()
31
+ {
32
+ return `window.dcpConfig = {
33
+ scheduler: {
34
+ location: new URL('${dcpConfig.scheduler.location.href}'),
35
+ },
36
+ const listenUrl = dcpConfig.webConsole.listen;
37
+ worker: (${JSON.stringify(dcpConfig.worker)})
38
+ }`;
39
+ }
40
+
41
+ /**
42
+ * HTTP server
43
+ */
44
+ var server = http.createServer(handleHttpRequest).unref();
45
+ function handleHttpRequest(request, response)
46
+ {
47
+ if (request.url === '/etc/dcp-config.js')
48
+ {
49
+ response.setHeader('Content-Type', magic.js);
50
+ response.end(makeSkeletonConfig());
51
+ return;
52
+ }
53
+
54
+ if (request.url === '/etc/dcp-config.kvin')
55
+ {
56
+ response.setHeader('Content-Type', magic.kvin);
57
+ response.end('(' + JSON.stringify(dcpConfig) + ')');
58
+ return;
59
+ }
60
+
61
+ var filename = (request.url.slice(1) || 'index.html');
62
+ const contentType = magic[path.extname(filename).slice(1)] || 'text/plain';
63
+ var errorStatus, contentDir;
64
+
65
+ if (!filename.startsWith('node_modules/'))
66
+ contentDir = wwwDir;
67
+ else
68
+ {
69
+ contentDir = nmDir;
70
+ filename = filename.slice(13);
71
+ }
72
+
73
+ filename = path.resolve(contentDir, filename);
74
+ console.log({contentDir,filename,requestUrl:request.url});
75
+ if (!filename.startsWith(contentDir) || filename[0] !== '/')
76
+ errorStatus = 401;
77
+ else if (!fs.existsSync(filename))
78
+ errorStatus = 404;
79
+
80
+ if (errorStatus)
81
+ {
82
+ response.setHeader('Content-Type', 'text/plain');
83
+ response.statusCode = errorStatus;
84
+ response.end(`${errorStatus} accessing ${filename}`);
85
+ return;
86
+ }
87
+
88
+ response.setHeader('Content-Type', contentType);
89
+ response.end(fs.readFileSync(filename));
90
+ }
91
+
92
+ server.listen({ host: listenHost, port: listenUrl.port }, function(server) {
93
+ console.log(`listening on port ${listenUrl.href}`);
94
+ });
95
+
96
+ /**
97
+ * WebSocket server
98
+ */
99
+ const WebSocketServer = require('websocket').server;
100
+ const wsServer = new WebSocketServer({
101
+ httpServer: server,
102
+ autoAcceptConnections: false
103
+ });
104
+
105
+ wsServer.on('request', handleWssRequest);
106
+ function handleWssRequest(request)
107
+ {
108
+ const connection = request.accept(null, request.origin);
109
+ connection.socket.unref();
110
+ clients.push(connection);
111
+
112
+ const write = connection.send;
113
+ const read = connection.socket.read;
114
+
115
+ connection.send(JSON.stringify({ type: 'log', facility: 'debug', message: 'hello, world' }));
116
+ connection.on('message', messageHandler);
117
+ connection.on('close', closeHandler);
118
+
119
+ function messageHandler(message)
120
+ {
121
+ console.log('message:', message)
122
+ console.log('this:', this);
123
+ }
124
+
125
+ function closeHandler()
126
+ {
127
+ }
128
+ }