Skip to content
Snippets Groups Projects
Commit 0544e033 authored by Rob Swindell's avatar Rob Swindell :speech_balloon:
Browse files

Merge branch 'Ree/node-spy-2' into 'master'

Refactor the node spy page

See merge request main/sbbs!314
parents 425a5b46 399aaa78
No related branches found
No related tags found
No related merge requests found
Pipeline #4637 passed
<?
// Ensure the user is a sysop, mqtt broker handles sensitive info that should not be available to non-sysop users
// (Ideally the page loading this component will check for SysOp status, but it's included here to be super safe)
if (!user.is_sysop) {
?>
<!-- TODOZ Translate -->
<div class="alert alert-danger"><h3>You must be a SysOp to use MQTT-based features.</h3></div>
<?
exit();
}
var request = require({}, settings.web_lib + 'request.js', 'request');
// Check for form posting back mqtt ports
var mqtt_post_error = '';
if (request.get_param('mqtt_ws_port') && request.get_param('mqtt_wss_port')) {
var new_ws_port = parseInt(request.get_param('mqtt_ws_port'), 10);
var new_wss_port = parseInt(request.get_param('mqtt_wss_port'), 10);
if ((new_ws_port >= 1) && (new_ws_port <= 65535) && (new_wss_port >= 1) && (new_wss_port <= 65535)) {
var f = new File(system.ctrl_dir + 'modopts.ini');
f.open("r+");
f.iniSetValue('web', 'mqtt_ws_port', new_ws_port);
f.iniSetValue('web', 'mqtt_wss_port', new_wss_port);
f.close();
settings.mqtt_ws_port = new_ws_port;
settings.mqtt_wss_port = new_wss_port;
} else {
mqtt_post_error = 'ERROR: Ports must be in the range of 1 to 65535';
}
}
// Read mqtt settings
var f = new File(system.ctrl_dir + 'main.ini');
f.open("r");
var broker_addr = f.iniGetValue('MQTT', 'Broker_addr', 'localhost');
var broker_enabled = f.iniGetValue('MQTT', 'Enabled', false);
var broker_password = f.iniGetValue('MQTT', 'Password', '');
var broker_username = f.iniGetValue('MQTT', 'Username', '');
var broker_ws_port = settings.mqtt_ws_port;
var broker_wss_port = settings.mqtt_wss_port;
f.close();
// Abort if the mqtt broker is not enabled
if (!broker_enabled) {
?>
<!-- TODOZ Translate -->
<div class="alert alert-danger"><h3>You must enable the MQTT broker in SCFG -> Networks -> MQTT before you can use the node spy</h3></div>
<?
exit();
}
// Display configuration form if mqtt ws/wss ports are not set yet
if (!broker_ws_port || !broker_wss_port) {
?>
<!-- TODOZ Translate -->
<div class="container">
<div class="col-lg-6 col-lg-offset-3 col-md-7 col-md-offset-3 col-sm-9 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3>Missing MQTT port settings</h3>
</div>
<div class="panel-body">
<p>Please provide the WebSocket ports your MQTT server is listening on:</p>
<form method="POST" class="form-horizontal">
<? if (mqtt_post_error) { ?>
<div class="alert alert-danger"><? write(mqtt_post_error); ?></div>
<? } ?>
<div class="form-group">
<label for="mqtt_ws_port" class="col-sm-3 control-label">WS:// port</label>
<div class="col-sm-9">
<input type="text" id="mqtt_ws_port" name="mqtt_ws_port" class="form-control" value="<? write(broker_ws_port === undefined ? '' : broker_ws_port); ?>" />
</div>
</div>
<div class="form-group">
<label for="mqtt_wss_port" class="col-sm-3 control-label">WSS:// port</label>
<div class="col-sm-9">
<input type="text" id="mqtt_wss_port" name="mqtt_wss_port" class="form-control" value="<? write(broker_wss_port === undefined ? '' : broker_wss_port); ?>" />
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<p>
Alternatively, you can edit <strong><? write(system.ctrl_dir + 'modopts.ini'); ?></strong> and add <strong>mqtt_ws_port</strong> and
<strong>mqtt_wss_port</strong> keys to the <strong>[web]</strong> section.
</p>
</div>
</div>
</div>
</div>
<?
exit();
}
?>
<!-- A hidden-by-default div with a message shown when ws:// connections fail -->
<!-- TODOZ Translate -->
<div class="alert alert-danger" id="mqtt-ws-reconnecting-message" style="display: none;">
<p class="lead">
Looks like your browser is having troubles connecting to the MQTT server over ws://. A few common causes:
</p>
<ul>
<li>The MQTT broker is offline.</li>
<li>The wrong hostname was specified in SCFG -> Networks -> MQTT (currently=<? write(broker_addr); ?>).</li>
<li>The wrong port was specified for the mqtt_ws_port setting in modopts.ini (currently=<? write(broker_ws_port); ?>).</li>
<li>A firewall is blocking the connection from your current location.</li>
</ul>
<p>
Double-check your settings and try reloading this page.
</p>
</div>
<!-- A hidden-by-default div witha message sown when wss:// connections fail -->
<!-- TODOZ Translate -->
<div class="alert alert-danger" id="mqtt-wss-reconnecting-message" style="display: none;">
<p class="lead">
Looks like your browser is having troubles connecting to the MQTT server over wss://. A few common causes:
</p>
<ul>
<li>The MQTT broker is offline.</li>
<li>The wrong hostname was specified in SCFG -> Networks -> MQTT (currently=<? write(broker_addr); ?>).</li>
<li>The wrong port was specified for the mqtt_wss_port setting in modopts.ini (currently=<? write(broker_wss_port); ?>).</li>
<li>The SSL cert has expired.</li>
<li>You're connecting to the broker via a hostname not found in the SSL cert.</li>
<li>You're connecting to the broker via an IP address.</li>
<li>A firewall is blocking the connection from your current location.</li>
</ul>
<p>
You can click the link below to try accessing the MQTT broker in a new tab/window, which might shed some light on what is going wrong,
and the in the case of an untrusted cert, allow you to accept it (e.g. by clicking Proceed in Chrome):<br />
<a href="" id="mqtt-wss-reconnecting-link" target="_blank">Access the MQTT server</a>
</p>
</div>
<script src="https://unpkg.com/mqtt@5.0.2/dist/mqtt.min.js"></script>
<script>
// Set javascript variables for system variables referenced by mqtt.js
var system_qwk_id = '<? write(system.qwk_id); ?>';
var broker_addr = '<? write(broker_addr); ?>';
var broker_password = '<? write(broker_password); ?>';
var broker_username = '<? write(broker_username); ?>';
var broker_ws_port = <? write(broker_ws_port); ?>;
var broker_wss_port = <? write(broker_wss_port); ?>;
</script>
<script src="./js/mqtt.js"></script>
<!--NO_SIDEBAR:Node Spy-->
<?
// Ensure the user is a sysop to avoid normal users from spying
if (!user.is_sysop) {
?>
<div class="alert alert-danger"><h3>You must be a SysOp to view the node spy</h3></div> <!-- TODOX Translate -->
<?
exit();
}
// Load fTelnet-related files
load(settings.web_lib + 'ftelnet.js');
load('ftelnethelper.js');
?>
<style>
.fTelnetStatusBar { display : none !important; } /* Don't show the status bar, we only want to use fTelnet for display purposes not connect purposes */
div.active { background-color: #ddd; padding-bottom: 15px; } /* Highlight the div that has keyboard focus */
label.normal { font-weight: normal !important; margin-bottom: 0 !important; } /* Sanely format the label elements */
/* Introduce a col-xl-6 to display two columns on large screens -- allows seeing all 4 of my nodes at once */
.col-xl-6 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
@media (min-width: 1600px) {
.col-xl-6 {
float: left;
width: 50%;
}
}
</style>
<h1 align="center">Node Spy</h1>
<? loadComponent('mqtt.xjs'); ?>
<!-- Multiple fTelnet instances (one per node) will be added to this wrapper div -->
<div id="ClientsWrapper"></div>
<script id="fTelnetScript" src="<? write(get_url()); ?>"></script>
<script src="./js/utf8_cp437.js"></script>
<script>
// Set javascript variables for system variables referenced by nodespy.js
var system_nodes = <? write(system.nodes); ?>;
var system_qwk_id = '<? write(system.qwk_id); ?>';
</script>
<script src="./js/nodespy.js"></script>
<!--NO_SIDEBAR:Node Spy-->
<?xjs
// Ensure the user is a sysop to avoid normal users from spying
if (!user.is_sysop) {
?>
<div class="alert alert-danger"><h3>You must be a SysOp to view the node spy</h3></div> <!-- TODOX Translate -->
<?xjs
exit();
}
// Read mqtt settings
var f = new File(system.ctrl_dir + 'main.ini');
f.open("r");
var broker_addr = f.iniGetValue('MQTT', 'Broker_addr', 'localhost');
var broker_enabled = f.iniGetValue('MQTT', 'Enabled', false);
var broker_password = f.iniGetValue('MQTT', 'Password', '');
var broker_username = f.iniGetValue('MQTT', 'Username', '');
var broker_ws_port = settings.mqtt_ws_port;
var broker_wss_port = settings.mqtt_wss_port;
// Abort if the mqtt broker is not enabled
if (!broker_enabled) {
?>
<div class="alert alert-danger"><h3>You must enable the MQTT broker in SCFG -> Networks -> MQTT before you can use the node spy</h3></div> <!-- TODOX Translate -->
<?xjs
exit();
}
// Abort if no ws(s) port set
if (!broker_ws_port || !broker_wss_port) {
?>
<div class="alert alert-danger"><h3>You must specify the MQTT broker's WS (mqtt_ws_port) and WSS (mqtt_wss_port) ports in the [web] section of <?xjs write(system.ctrl_dir + 'modopts.ini'); ?> before you can use the node spy</h3></div> <!-- TODOX Translate -->
<?xjs
exit();
}
// Load fTelnet-related files
load(settings.web_lib + 'ftelnet.js');
load('ftelnethelper.js');
?>
<style>
.fTelnetStatusBar { display : none !important; } /* Don't show the status bar, we only want to use fTelnet for display purposes not connect purposes */
div.active { background-color: #ddd; padding-bottom: 15px; } /* Highlight the div that has keyboard focus */
label { font-weight: normal !important; margin-bottom: 0 !important; } /* Sanely format the label elements */
/* Introduce a col-xl-6 to display two columns on large screens -- allows seeing all 4 of my nodes at once */
.col-xl-6 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
@media (min-width: 1600px) {
.col-xl-6 {
float: left;
width: 50%;
}
}
</style>
<h1 align="center">Node Spy</h1>
<!-- A hidden-by-default div with a link to access the MQTT server in the browser, to see if the cert is trusted -->
<div id="HttpsMqttDiv" style="display: none;">
<!-- TODOX Translate -->
Looks like your browser is having troubles connecting to the MQTT server over WSS. A few common causes:
<ul>
<li>The MQTT broker is offline</li>
<li>The SSL cert has expired</li>
<li>You're connecting to the broker via a hostname not found in the SSL cert</li>
<li>You're connecting to the broker via an IP address</li>
</ul>
You can click the link below to try accessing the MQTT broker in a new tab/window, which might shed some light on what is going wrong, and the in the case of an
untrusted cert, allow you to accept it (eg clicking Proceed in Chrome):<br />
<a href="" id="HttpsMqttLink" target="_blank">Access the MQTT server</a>
</div>
<!-- Multiple fTelnet instances (one per node) will be added to this wrapper div -->
<div id="ClientsWrapper"></div>
<script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script>
<script src="https://unpkg.com/mqtt@5.0.2/dist/mqtt.min.js"></script>
<script src="./js/utf8_cp437.js"></script>
<script>
var charsets = [];
var fTelnetControls = [];
var mqttClient;
// Check if we're targeting a specific node
const urlParams = new URLSearchParams(window.location.search);
const targetNode = parseInt(urlParams.get('node'), 10);
// Connect to the mqtt broker and subscribe to some topics
function connect_to_mqtt_broker() {
const options = {
keepalive: 60,
clientId: '<?xjs write(system.qwk_id); ?>' + Math.random().toString(16).substr(2, 8),
username: '<?xjs write(broker_username); ?>',
password: '<?xjs write(broker_password); ?>',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 5000,
connectTimeout: 30 * 1000,
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false
},
}
// Build the host string to connect to based on whether ws:// or wss:// is required
var protocol = location.protocol === 'https:' ? 'wss' : 'ws';
var port = location.protocol === 'https:' ? <?xjs write(broker_wss_port); ?> : <?xjs write(broker_ws_port); ?>;
const host = protocol + '://<?xjs write(broker_addr) ?>:' + port + '/mqtt';
// Set the debugging link to a modified version of the host string
$('#HttpsMqttLink').attr('href', host.replace('wss://', 'https://'));
// Connect to the mqtt broker
log_ftelnet('Connecting to mqtt broker (' + host + ')');
mqttClient = mqtt.connect(host, options);
// Subscribe to topics on connect
mqttClient.on('connect', () => {
log_ftelnet('Connected');
$('#HttpsMqttDiv').hide();
for (var node in fTelnetControls) {
mqttClient.subscribe('sbbs/<?xjs write(system.qwk_id) ?>/node/' + node + '', { qos: 2 });
mqttClient.subscribe('sbbs/<?xjs write(system.qwk_id) ?>/node/' + node + '/output', { qos: 2 });
mqttClient.subscribe('sbbs/<?xjs write(system.qwk_id) ?>/node/' + node + '/terminal', { qos: 2 });
}
});
// Display an error when connection fails
mqttClient.on('error', (err) => {
log_ftelnet('Connection error: ', err);
mqttClient.end();
});
// Handle messages for subscribed topics
mqttClient.on('message', (topic, message, packet) => {
var match = topic.match(/node[\/](\d*)([\/](output|terminal))?/);
var node = match[1];
var messageType = match[2];
switch (messageType) {
case undefined:
// message contains the node's current activity (ie waiting for caller, or user is running an external, etc)
$('#Status' + node).html(message.toString());
break;
case '/output':
// message contains the raw output being sent to the user, so write it to the fTelnet instance
var buf = new Uint8Array(message);
var str = '';
for (var i = 0; i < message.length; i++) {
str += String.fromCharCode(buf[i]);
}
// UTF-8 decode, if necessary
if (charsets[node] === 'UTF-8') {
str = utf8_cp437(str);
}
fTelnetControls[node]._Ansi.Write(str);
break;
case '/terminal':
// message contains tab-separated terminal information, most importantly the column and row count as the first
// and second elements, so resize fTelnet and tell it to reload the best font based on the new size.
var arr = message.toString().split('\t');
fTelnetControls[node]._Crt.SetScreenSize(parseInt(arr[0], 10), parseInt(arr[1], 10));
fTelnetControls[node]._Crt.SetFont(fTelnetControls[node]._Crt.Font.Name);
charsets[node] = arr[4];
// TODOX When arr[4] is 'CBM-ASCII' things get messy -- did I add support for PETSCII to fTelnet?
break;
}
});
// Display a message when the connection drops and the client is attempting to reconnect
// Also show the debugging message if https is being used, because the sysop may need to resolve issues with their ssl cert
mqttClient.on('reconnect', () => {
log_ftelnet('Reconnecting...');
if (location.protocol === 'https:') {
$('#HttpsMqttDiv').show();
}
});
}
// Create an fTelnet instance for each node
function create_ftelnet_instances() {
// Loop through the node range
for (var node = 1; node <= <?xjs write(system.nodes) ?>; node++) {
if (!targetNode || (targetNode === node)) {
// Build a node-specific link
var nodeUrl = location.href;
if (!nodeUrl.includes("&node=")) {
nodeUrl += '&node=' + node;
}
// Build a div containing the node status, keyboard checkbox, and fTelnet instance
$('#ClientsWrapper').append(`
<div id="Client${node}" class="${targetNode ? '' : 'col-xl-6'}">
<h4 style="text-align: center;">
<a href="${nodeUrl}">Node ${node}</a> -
<span id="Status${node}">Status Unknown</span> &nbsp; &nbsp;
<span style="white-space: nowrap;">
<input type="checkbox" class="keyboard-input" id="chkKeyboard${node}" value="${node}" />
<label for="chkKeyboard${node}">Enable Keyboard Input</label>
</span>
</h4>
<div id="fTelnetContainer${node}" class="fTelnetContainer"></div>
</div>
`);
// And initialize a new fTelnet instance
var Options = new fTelnetOptions();
Options.AllowModernScrollback = false;
Options.BareLFtoCRLF = false;
Options.BitsPerSecond = 57600;
Options.ConnectionType = 'telnet';
Options.Emulation = 'ansi-bbs';
Options.Enter = '\r';
Options.Font = 'CP437';
Options.ForceWss = false;
Options.Hostname = 'unused';
Options.LocalEcho = false;
Options.Port = 11235;
Options.ScreenColumns = 80;
Options.ScreenRows = 25;
Options.SplashScreen = ' ';
fTelnetControls[node] = new fTelnetClient('fTelnetContainer' + node, Options);
charsets[node] = 'CP437';
}
}
}
// Setup the keyboard handler
function init_keyboard_handler() {
// Handle clicks on the keyboard checkboxes
$('.keyboard-input').click(function() {
var selectedNode = this.value;
// Highlight the new selected div, and un-highlight+un-check the other divs and checkboxes
for (var node in fTelnetControls) {
if ((node === selectedNode) && this.checked) {
$('#Client' + node).addClass('active');
} else {
$('#Client' + node).removeClass('active');
$('.keyboard-input[value=' + node + ']').removeAttr('checked');
}
}
});
// Handle keydown event, which is where control keys and special keys are handled
// Code is copy/pasted (with slight modifications) from fTelnet
window.addEventListener('keydown', function (ke) {
var node = $('.keyboard-input:checked').attr('value');
if (node) {
var keyString = '';
if (ke.ctrlKey) {
// Handle control + letter keys
if ((ke.keyCode >= 65) && (ke.keyCode <= 90)) {
keyString = String.fromCharCode(ke.keyCode - 64);
} else if ((ke.keyCode >= 97) && (ke.keyCode <= 122)) {
keyString = String.fromCharCode(ke.keyCode - 96);
}
} else {
switch (ke.keyCode) {
// Handle special keys
case KeyboardKeys.BACKSPACE: keyString = '\b'; break;
case KeyboardKeys.DELETE: keyString = '\x7F'; break;
case KeyboardKeys.DOWN: keyString = '\x1B[B'; break;
case KeyboardKeys.END: keyString = '\x1B[K'; break;
case KeyboardKeys.ENTER: keyString = '\r\n'; break;
case KeyboardKeys.ESCAPE: keyString = '\x1B'; break;
case KeyboardKeys.F1: keyString = '\x1BOP'; break;
case KeyboardKeys.F2: keyString = '\x1BOQ'; break;
case KeyboardKeys.F3: keyString = '\x1BOR'; break;
case KeyboardKeys.F4: keyString = '\x1BOS'; break;
case KeyboardKeys.F5: keyString = '\x1BOt'; break;
case KeyboardKeys.F6: keyString = '\x1B[17~'; break;
case KeyboardKeys.F7: keyString = '\x1B[18~'; break;
case KeyboardKeys.F8: keyString = '\x1B[19~'; break;
case KeyboardKeys.F9: keyString = '\x1B[20~'; break;
case KeyboardKeys.F10: keyString = '\x1B[21~'; break;
case KeyboardKeys.F11: keyString = '\x1B[23~'; break;
case KeyboardKeys.F12: keyString = '\x1B[24~'; break;
case KeyboardKeys.HOME: keyString = '\x1B[H'; break;
case KeyboardKeys.INSERT: keyString = '\x1B@'; break;
case KeyboardKeys.LEFT: keyString = '\x1B[D'; break;
case KeyboardKeys.PAGE_DOWN: keyString = '\x1B[U'; break;
case KeyboardKeys.PAGE_UP: keyString = '\x1B[V'; break;
case KeyboardKeys.RIGHT: keyString = '\x1B[C'; break;
case KeyboardKeys.SPACE: keyString = ' '; break;
case KeyboardKeys.TAB: keyString = '\t'; break;
case KeyboardKeys.UP: keyString = '\x1B[A'; break;
}
}
// If we have a keyString, then publish it so sbbs sees it as input
if (keyString) {
mqttClient.publish('sbbs/<?xjs write(system.qwk_id) ?>/node/' + node + '/input', keyString, { qos: 1, retain: false });
}
// If we have a keyString, or CTRL is being held, prevent further handling (so the keypress event won't be hit next)
if ((keyString) || (ke.ctrlKey)) {
ke.preventDefault();
}
}
});
// Handle keypress event, which is where standard keys are handled
// Code is copy/pasted (with slight modifications) from fTelnet
window.addEventListener('keypress', function (ke) {
var node = $('.keyboard-input:checked').attr('value');
if (node) {
if (ke.charCode >= 33) {
mqttClient.publish('sbbs/<?xjs write(system.qwk_id) ?>/node/' + node + '/input', String.fromCharCode(ke.charCode), { qos: 1, retain: false });
}
}
});
}
// Log a message to each of the fTelnet instances
function log_ftelnet(message) {
for (var node in fTelnetControls) {
fTelnetControls[node]._Crt.WriteLn(message);
}
}
// Add an onload handler to setup the node spy
window.addEventListener('load', (event) => {
create_ftelnet_instances();
init_keyboard_handler();
connect_to_mqtt_broker();
});
</script>
\ No newline at end of file
var mqtt_client;
// Connect to the mqtt broker and subscribe to some topics
function mqtt_connect(topics, message_callback, log_callback) {
const options = {
keepalive: 60,
clientId: system_qwk_id + Math.random().toString(16).substr(2, 8),
username: broker_username,
password: broker_password,
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 5000,
connectTimeout: 30 * 1000,
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false
},
};
// Build the host string to connect to based on whether ws:// or wss:// is required
var protocol = location.protocol === 'https:' ? 'wss' : 'ws';
var port = location.protocol === 'https:' ? broker_wss_port : broker_ws_port;
const host = protocol + '://' + broker_addr + ':' + port + '/mqtt';
// Set the debugging link to a modified version of the host string
$('#mqtt-wss-reconnecting-link').attr('href', host.replace('wss://', 'https://'));
// Connect to the mqtt broker
log_callback('Connecting to mqtt broker (' + host + ')');
mqtt_client = mqtt.connect(host, options);
// Subscribe to topics on connect
mqtt_client.on('connect', () => {
log_callback('Connected');
for (var i = 0; i < topics.length; i++) {
mqtt_client.subscribe(topics[i], { qos: 2 });
}
$('#mqtt-ws-reconnecting-message').hide();
$('#mqtt-wss-reconnecting-message').hide();
});
// Display an error when connection fails
mqtt_client.on('error', (err) => {
log_callback('Connection error: ', err);
mqtt_client.end();
});
// Handle messages for subscribed topics
mqtt_client.on('message', (topic, message, packet) => {
message_callback(topic, message, packet);
});
// Display a message when the connection drops and the client is attempting to reconnect
// Also show the debugging message if https is being used, because the sysop may need to resolve issues with their ssl cert
mqtt_client.on('reconnect', () => {
log_callback('Reconnecting...');
if (location.protocol === 'https:') {
$('#mqtt-wss-reconnecting-message').show();
} else {
$('#mqtt-ws-reconnecting-message').show();
}
});
}
function mqtt_publish(topic, value) {
mqtt_client.publish(topic, value, { qos: 1, retain: false });
}
var spy_nodes = [];
// Check if we're targeting a specific node
const urlParams = new URLSearchParams(window.location.search);
const targetNode = parseInt(urlParams.get('node'), 10);
// Create an fTelnet instance for each node
function create_ftelnet_instances() {
// Loop through the node range
for (var node = 1; node <= system_nodes; node++) {
if (!targetNode || (targetNode === node)) {
// Build a node-specific link
var nodeUrl = location.href;
if (!nodeUrl.includes("&node=")) {
nodeUrl += '&node=' + node;
}
// Build a div containing the node status, keyboard checkbox, and fTelnet instance
$('#ClientsWrapper').append(`
<div id="Client${node}" class="${targetNode ? '' : 'col-xl-6'}">
<h4 style="text-align: center;">
<a href="${nodeUrl}">Node ${node}</a> -
<span id="Status${node}">Status Unknown</span> &nbsp; &nbsp;
<span style="white-space: nowrap;">
<input type="checkbox" class="keyboard-input" id="chkKeyboard${node}" value="${node}" />
<label class="normal" for="chkKeyboard${node}">Enable Keyboard Input</label>
</span>
</h4>
<div id="fTelnetContainer${node}" class="fTelnetContainer"></div>
</div>
`);
// And initialize a new fTelnet instance
var Options = new fTelnetOptions();
Options.AllowModernScrollback = false;
Options.BareLFtoCRLF = false;
Options.BitsPerSecond = 57600;
Options.ConnectionType = 'telnet';
Options.Emulation = 'ansi-bbs';
Options.Enter = '\r';
Options.Font = 'CP437';
Options.ForceWss = false;
Options.Hostname = 'unused';
Options.LocalEcho = false;
Options.Port = 11235;
Options.ScreenColumns = 80;
Options.ScreenRows = 25;
Options.SplashScreen = ' ';
spy_nodes[node] = {
charset: 'CP437',
ftelnet: new fTelnetClient('fTelnetContainer' + node, Options),
};
}
}
}
// Setup the keyboard handler
function init_keyboard_handler() {
// Handle clicks on the keyboard checkboxes
$('.keyboard-input').click(function() {
var selectedNode = this.value;
// Highlight the new selected div, and un-highlight+un-check the other divs and checkboxes
for (var node in spy_nodes) {
if ((node === selectedNode) && this.checked) {
$('#Client' + node).addClass('active');
} else {
$('#Client' + node).removeClass('active');
$('.keyboard-input[value=' + node + ']').removeAttr('checked');
}
}
});
// Handle keydown event, which is where control keys and special keys are handled
// Code is copy/pasted (with slight modifications) from fTelnet
window.addEventListener('keydown', function (ke) {
var node = $('.keyboard-input:checked').attr('value');
if (node) {
var keyString = '';
if (ke.ctrlKey) {
// Handle control + letter keys
if ((ke.keyCode >= 65) && (ke.keyCode <= 90)) {
keyString = String.fromCharCode(ke.keyCode - 64);
} else if ((ke.keyCode >= 97) && (ke.keyCode <= 122)) {
keyString = String.fromCharCode(ke.keyCode - 96);
}
} else {
switch (ke.keyCode) {
// Handle special keys
case KeyboardKeys.BACKSPACE: keyString = '\b'; break;
case KeyboardKeys.DELETE: keyString = '\x7F'; break;
case KeyboardKeys.DOWN: keyString = '\x1B[B'; break;
case KeyboardKeys.END: keyString = '\x1B[K'; break;
case KeyboardKeys.ENTER: keyString = '\r\n'; break;
case KeyboardKeys.ESCAPE: keyString = '\x1B'; break;
case KeyboardKeys.F1: keyString = '\x1BOP'; break;
case KeyboardKeys.F2: keyString = '\x1BOQ'; break;
case KeyboardKeys.F3: keyString = '\x1BOR'; break;
case KeyboardKeys.F4: keyString = '\x1BOS'; break;
case KeyboardKeys.F5: keyString = '\x1BOt'; break;
case KeyboardKeys.F6: keyString = '\x1B[17~'; break;
case KeyboardKeys.F7: keyString = '\x1B[18~'; break;
case KeyboardKeys.F8: keyString = '\x1B[19~'; break;
case KeyboardKeys.F9: keyString = '\x1B[20~'; break;
case KeyboardKeys.F10: keyString = '\x1B[21~'; break;
case KeyboardKeys.F11: keyString = '\x1B[23~'; break;
case KeyboardKeys.F12: keyString = '\x1B[24~'; break;
case KeyboardKeys.HOME: keyString = '\x1B[H'; break;
case KeyboardKeys.INSERT: keyString = '\x1B@'; break;
case KeyboardKeys.LEFT: keyString = '\x1B[D'; break;
case KeyboardKeys.PAGE_DOWN: keyString = '\x1B[U'; break;
case KeyboardKeys.PAGE_UP: keyString = '\x1B[V'; break;
case KeyboardKeys.RIGHT: keyString = '\x1B[C'; break;
case KeyboardKeys.SPACE: keyString = ' '; break;
case KeyboardKeys.TAB: keyString = '\t'; break;
case KeyboardKeys.UP: keyString = '\x1B[A'; break;
}
}
// If we have a keyString, then publish it so sbbs sees it as input
if (keyString) {
mqtt_publish('sbbs/' + system_qwk_id + '/node/' + node + '/input', keyString);
}
// If we have a keyString, or CTRL is being held, prevent further handling (so the keypress event won't be hit next)
if ((keyString) || (ke.ctrlKey)) {
ke.preventDefault();
}
}
});
// Handle keypress event, which is where standard keys are handled
// Code is copy/pasted (with slight modifications) from fTelnet
window.addEventListener('keypress', function (ke) {
var node = $('.keyboard-input:checked').attr('value');
if (node) {
if (ke.charCode >= 33) {
mqtt_publish('sbbs/' + system_qwk_id + '/node/' + node + '/input', String.fromCharCode(ke.charCode));
}
}
});
}
// Log a message to each of the fTelnet instances
function log_ftelnet(message) {
for (var node in spy_nodes) {
spy_nodes[node].ftelnet._Crt.WriteLn(message);
}
}
function mqtt_message(topic, message, packet) {
var match = topic.match(/node[\/](\d*)([\/](output|terminal))?/);
var node = match[1];
var messageType = match[2];
switch (messageType) {
case undefined:
// message contains the node's current activity (ie waiting for caller, or user is running an external, etc)
$('#Status' + node).html(message.toString());
break;
case '/output':
// message contains the raw output being sent to the user, so write it to the fTelnet instance
var buf = new Uint8Array(message);
var str = '';
for (var i = 0; i < message.length; i++) {
str += String.fromCharCode(buf[i]);
}
// UTF-8 decode, if necessary
if (spy_nodes[node].charset === 'UTF-8') {
str = utf8_cp437(str);
}
spy_nodes[node].ftelnet._Ansi.Write(str);
break;
case '/terminal':
// message contains tab-separated terminal information, most importantly the column and row count as the first
// and second elements, so resize fTelnet and tell it to reload the best font based on the new size.
var arr = message.toString().split('\t');
spy_nodes[node].ftelnet._Crt.SetScreenSize(parseInt(arr[0], 10), parseInt(arr[1], 10));
spy_nodes[node].ftelnet._Crt.SetFont(spy_nodes[node].ftelnet._Crt.Font.Name);
spy_nodes[node].charset = arr[4];
// TODOX When arr[4] is 'CBM-ASCII' things get messy -- did I add support for PETSCII to fTelnet?
break;
}
}
// Add an onload handler to setup the node spy
window.addEventListener('load', (event) => {
create_ftelnet_instances();
init_keyboard_handler();
var topics = [];
for (var node in spy_nodes) {
topics.push('sbbs/' + system_qwk_id + '/node/' + node + '');
topics.push('sbbs/' + system_qwk_id + '/node/' + node + '/output');
topics.push('sbbs/' + system_qwk_id + '/node/' + node + '/terminal');
}
mqtt_connect(topics, mqtt_message, log_ftelnet);
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment