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

Add a node spy to the web interface

parent 48d92b2d
No related branches found
No related tags found
No related merge requests found
......@@ -263,5 +263,7 @@ function writePage(page) {
var ini = getWebCtrl(pp.replace(file_getname(page), ''));
if ((typeof ini === "boolean" && !ini) || webCtrlTest(ini, page)) {
write(getPage(page));
}
} else {
write('<div class="alert alert-danger"><h3>You do not have access to this page</h3></div>'); // TODOX Translate
}
}
<!--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
AccessRequirements = level 90
Authorization = Digest
[*nodespy.xjs]
AccessRequirements = level 90
[*userlist.xjs]
AccessRequirements = LEVEL 50 AND REST NOT G
This diff is collapsed.
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