Skip to content
Snippets Groups Projects
Commit cee1cd17 authored by Rick Parrish's avatar Rick Parrish Committed by Rob Swindell
Browse files

Refactor the node spy page

parent ba6a3578
No related branches found
No related tags found
No related merge requests found
<?
// 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.
Finish editing this message first!
Please register or to comment