diff --git a/webv4/components/mqtt.xjs b/webv4/components/mqtt.xjs index 723573919cfa0b6bcdb9fcc5a674e075549355eb..d552fc873918f79160b37c2e0d0e594b8b26828c 100644 --- a/webv4/components/mqtt.xjs +++ b/webv4/components/mqtt.xjs @@ -1,31 +1,36 @@ <? // 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(); - } + load(xjs_compile(settings.web_components + 'sysop-required.xjs')); 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')) { + if (request.get_param('mqtt_hostname') && request.get_param('mqtt_ws_port') && request.get_param('mqtt_wss_port')) { + var new_hostname = request.get_param('mqtt_hostname'); 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; + if (f.open("r+")) { + try { + f.iniSetValue('web', 'mqtt_hostname', new_hostname); + f.iniSetValue('web', 'mqtt_ws_port', new_ws_port); + f.iniSetValue('web', 'mqtt_wss_port', new_wss_port); + + settings.mqtt_hostname = new_hostname; + settings.mqtt_ws_port = new_ws_port; + settings.mqtt_wss_port = new_wss_port; + } catch (error) { + mqtt_post_error = 'ERROR: ' + error.message; + } finally { + f.close(); + } + } else { + mqtt_post_error = 'ERROR: Unable to open modopts.ini for writing'; + } } else { mqtt_post_error = 'ERROR: Ports must be in the range of 1 to 65535'; } @@ -33,14 +38,19 @@ // 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(); + if (f.open("r")) { + try { + var broker_enabled = f.iniGetValue('MQTT', 'Enabled', false); + var broker_password = f.iniGetValue('MQTT', 'Password', ''); + var broker_username = f.iniGetValue('MQTT', 'Username', ''); + + var broker_addr = settings.mqtt_hostname; + var broker_ws_port = settings.mqtt_ws_port; + var broker_wss_port = settings.mqtt_wss_port; + } finally { + f.close(); + } + } // Abort if the mqtt broker is not enabled if (!broker_enabled) { @@ -51,24 +61,31 @@ exit(); } - // Display configuration form if mqtt ws/wss ports are not set yet - if (!broker_ws_port || !broker_wss_port) { + // Display configuration form if mqtt addr and ws/wss ports are not set yet + if (!broker_addr || !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> + <h3>Missing MQTT settings</h3> </div> <div class="panel-body"> - <p>Please provide the WebSocket ports your MQTT server is listening on:</p> + <p>Please provide the hostname and 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_hostname" class="col-sm-3 control-label">Hostname</label> + <div class="col-sm-9"> + <input type="text" id="mqtt_hostname" name="mqtt_hostname" class="form-control" value="<? write(broker_addr === undefined ? '' : broker_addr); ?>" /> + </div> + </div> + <div class="form-group"> <label for="mqtt_ws_port" class="col-sm-3 control-label">WS:// port</label> <div class="col-sm-9"> @@ -92,8 +109,8 @@ </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. + Alternatively, you can edit <strong><? write(system.ctrl_dir + 'modopts.ini'); ?></strong> and add <strong>mqtt_hostname</strong>, + <strong>mqtt_ws_port</strong> and <strong>mqtt_wss_port</strong> keys to the <strong>[web]</strong> section. </p> </div> </div> @@ -115,7 +132,7 @@ <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 hostname was specified for the mqtt_hostname setting in modopts.ini (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> @@ -134,7 +151,7 @@ <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 hostname was specified for the mqtt_hostname setting in modopts.ini (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> diff --git a/webv4/components/sysop-required.xjs b/webv4/components/sysop-required.xjs new file mode 100644 index 0000000000000000000000000000000000000000..5b955f947d48c2e2597b8101935a8c6ad778ad73 --- /dev/null +++ b/webv4/components/sysop-required.xjs @@ -0,0 +1,10 @@ +<? + // Ensure the user is a sysop to avoid normal users from accessing the page this component is embedded on + if (!user.is_sysop) { +?> + <!-- TODOZ Translate --> + <div class="alert alert-danger"><h3>You must be a SysOp to view this page.</h3></div> +<? + exit(); + } +?> diff --git a/webv4/components/webmonitor/clients-panel.xjs b/webv4/components/webmonitor/clients-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..836a99f7fa85c33cec9b1b20501e03e54c9124cd --- /dev/null +++ b/webv4/components/webmonitor/clients-panel.xjs @@ -0,0 +1,19 @@ +<div class="tab-pane fade webmonitor-panel <? write(argv[0]); ?>" id="clientsTab"> + <div class="webmonitor-scrolling-wrapper" id="clientsTableWrapper"> + <table class="table table-bordered table-condensed table-hover table-striped"> + <thead> + <tr> + <th>Socket</th> + <th>Protocol</th> + <th>User</th> + <th>Address</th> + <th>Host Name</th> + <th>Port</th> + <th>Time</th> + </tr> + </thead> + <tbody id="clientsTable"> + </tbody> + </table> + </div> +</div> diff --git a/webv4/components/webmonitor/documentation-panel.xjs b/webv4/components/webmonitor/documentation-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..941e9ff414de0411c4cd8df3fbb654a48535c33f --- /dev/null +++ b/webv4/components/webmonitor/documentation-panel.xjs @@ -0,0 +1,37 @@ +<div class="tab-pane fade webmonitor-panel <? write(argv[0]); ?>" id="documentationTab"> + <div class="webmonitor-scrolling-wrapper"> + <h3>Enabling the Configure buttons</h3> + <p> + By default, the configure buttons are not enabled. This is because it's still an experimental feature and not recommended + for use yet. But if you'd like to test it out, <strong>and have backed up your sbbs.ini first</strong>, you can enable + the configure buttons by adding this key/value to the [web] section of ctrl/modopts.ini: + </p> + <ul> + <li>webmonitor_configuration_enabled=true</li> + </ul> + <p class="small">NB: Only one tab of the Terminal Server Configuration is functional right now.</p> + + <h3>Customizing the panel layout</h3> + <p> + By default, my preferred layout is used for the panels on the Web Monitor interface. + If you'd like to restore the default SBBSCTRL layout of the panels, you can edit your ctrl/modopts.ini and add these keys/values to the [web] section: + </p> + <ul> + <li>webmonitor_quadrant_top_left=nodes,clients,statistics</li> + <li>webmonitor_quadrant_top_right=mail</li> + <li>webmonitor_quadrant_bottom_left=term,events</li> + <li>webmonitor_quadrant_bottom_right=srvc,ftp,web</li> + </ul> + <p> + Of course you can customize the layout by changing the order of the values, or even moving them to a different quadrant. And if you want + to hide a panel, just don't include it as a value in any of the four keys. + </p> + <p> + You may want to also include "documentation" as a value at the end of one of the four keys, just in case this panel is updated with new + information in the future. + </p> + <p> + There's also a new "recent-visitors" panel that you should account for if you're going to customize the four keys. + </p> + </div> +</div> diff --git a/webv4/components/webmonitor/events-panel.xjs b/webv4/components/webmonitor/events-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..d16b78f808440b8fde7defad5ea8341f4ae19352 --- /dev/null +++ b/webv4/components/webmonitor/events-panel.xjs @@ -0,0 +1,15 @@ +<div class="tab-pane fade webmonitor-panel <? write(argv[0]); ?>" id="eventsTab"> + <div class="webmonitor-scrolling-wrapper" id="eventsTableWrapper"> + <table class="table table-bordered table-condensed table-hover"> + <thead> + <tr> + <th>Date/Time</th> + <th>Level</th> + <th>Message</th> + </tr> + </thead> + <tbody id="eventsLog"> + </tbody> + </table> + </div> +</div> diff --git a/webv4/components/webmonitor/nodes-panel.xjs b/webv4/components/webmonitor/nodes-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..d61b4890aab4e0f1ccf2cda2aecb7d5327906fcf --- /dev/null +++ b/webv4/components/webmonitor/nodes-panel.xjs @@ -0,0 +1,24 @@ +<div class="tab-pane fade webmonitor-panel <? write(argv[0]); ?>" id="nodesTab"> + <div class="webmonitor-scrolling-wrapper" id="nodesTableWrapper"> + <table class="table table-bordered table-condensed table-hover table-striped"> + <thead> + <tr> + <th>Node</th> + <th>Status</th> + </tr> + </thead> + <tbody> + <? for (var node = 1; node <= system.nodes; node++) { ?> + <tr> + <td align="center" width="50"><? write(node); ?></td> + <td> + <span id="nodeStatusLabel<? write(node); ?>">Unknown Status</span> + + <a href="./?page=More/001-nodespy.xjs&node=<? write(node); ?>" target="_blank" class="btn btn-primary btn-xs visible-hover"><span class="glyphicon glyphicon-search"></span> Spy</a> + </td> + </tr> + <? } ?> + </tbody> + </table> + </div> +</div> diff --git a/webv4/components/webmonitor/quadrant.xjs b/webv4/components/webmonitor/quadrant.xjs new file mode 100644 index 0000000000000000000000000000000000000000..35d4c0911039a644d7b7db5c24c4e50f8948cd27 --- /dev/null +++ b/webv4/components/webmonitor/quadrant.xjs @@ -0,0 +1,68 @@ +<? + var server_panels = ['ftp', 'mail', 'srvc', 'term', 'web']; + + // Read the modopts setting that controls what panels should appear in each quadrant + var panels = settings['webmonitor_quadrant_' + argv[0]]; + + // If there isn't a modopts setting for this quadrant, use the default + if (!panels) { + switch (argv[0]) { + case 'bottom_left': + panels = 'term,events'; + break; + case 'bottom_right': + panels = 'srvc,web,ftp,documentation'; + break; + case 'top_left': + panels = 'nodes,recent-visitors,statistics'; + break; + case 'top_right': + panels = 'clients,mail'; + break; + } + } + + // Split the comma-separated string into an array we'll loop through later + panels = panels.split(','); + + // Convert the panel name into a description (eg term into Terminal Server) + function get_panel_description(name) { + switch (name) { + case 'clients': return 'Clients'; + case 'documentation': return 'Documentation'; + case 'events': return 'Events'; + case 'ftp': return 'FTP Server'; + case 'mail': return 'Mail'; + case 'nodes': return 'Nodes'; + case 'recent-visitors': return 'Recent Visitors'; + case 'srvc': return 'Services'; + case 'statistics': return 'Statistics'; + case 'term': return 'Terminal Server'; + case 'web': return 'Web Server'; + default: return name; + } + } +?> + +<ul class="nav nav-tabs"> + <? + // Loop through the panel list to build the list of tabs + for (var i = 0; i < panels.length; i++) { + var className = i == 0 ? 'active' : ''; + var badge = panels[i] === 'clients' ? ' <span class="badge" id="' + panels[i] + 'CounterBadge">0</span>' : ''; + write('<li class="' + className + '"><a href="#' + panels[i] + 'Tab" data-toggle="tab">' + get_panel_description(panels[i]) + badge + '</a></li>'); + } + ?> +</ul> +<div class="tab-content"> + <? + // Loop through the panel list to load the proper component panels + for (var i = 0; i < panels.length; i++) { + if (server_panels.includes(panels[i])) { + load(xjs_compile(settings.web_components + 'webmonitor/server-panel.xjs'), panels[i], get_panel_description(panels[i]), i == 0 ? 'active in' : ''); + } else { + load(xjs_compile(settings.web_components + 'webmonitor/' + panels[i] + '-panel.xjs'), i == 0 ? 'active in' : ''); + } + } + ?> +</div> diff --git a/webv4/components/webmonitor/recent-visitors-panel.xjs b/webv4/components/webmonitor/recent-visitors-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..82cde0efeb734a6a8a7b6a8845a39cea5061b5b5 --- /dev/null +++ b/webv4/components/webmonitor/recent-visitors-panel.xjs @@ -0,0 +1,41 @@ +<? + var logonlist_lib = load({}, 'logonlist_lib.js'); + var ll = logonlist_lib.get(-10); +?> + +<div class="tab-pane fade webmonitor-panel <? write(argv[0]); ?>" id="recent-visitorsTab"> + <div class="webmonitor-scrolling-wrapper" id="recent-visitorsTableWrapper"> + <table class="table table-bordered table-condensed table-hover table-striped"> + <thead> + <tr> + <th>Date/Time</th> + <th>Alias</th> + <th>Name</th> + <th>Email</th> + <th>Location</th> + <th>Protocol</th> + </tr> + </thead> + <tbody id="recent-visitorsTable"> +<? + if (Array.isArray(ll)) { + ll.forEach(function (e) { +?> + <tr> + <td><? write(new Date(e.time * 1000).toLocaleString()); ?></td> + <td><? write(e.user.alias.replace(/</g, '<')); ?></td> + <td><? write(e.user.name.replace(/</g, '<')); ?></td> + <td><? write(e.user.netmail.replace(/</g, '<')); ?></td> + <td><? write(e.user.location.replace(/</g, '<')); ?></td> + <td><? write(e.user.connection); ?></td> + </tr> +<? + }); + } else { + write('<tr><td colspan="6" align="center">No recent visitors to display</td></tr>'); + } +?> + </tbody> + </table> + </div> +</div> diff --git a/webv4/components/webmonitor/server-panel.xjs b/webv4/components/webmonitor/server-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..89ec54bd8729eaaa076a8e1aae63628cbda9e956 --- /dev/null +++ b/webv4/components/webmonitor/server-panel.xjs @@ -0,0 +1,28 @@ +<? + var serverType = argv[0]; + var serverDescription = argv[1]; + var classNames = argv[2]; +?> + +<div class="tab-pane fade webmonitor-panel <? write(classNames); ?>" id="<? write(serverType); ?>Tab"> + <div class="webmonitor-toolbar-buttons"> + <button class="btn btn-default xbtn-sm" id="<? write(serverType); ?>StatusButton">Status: Unknown</button> + <button class="btn btn-default xbtn-sm webmonitor-recycle" data-server="<? write(serverType); ?>" data-toggle="tooltip" data-placement="bottom" title="Recycle <? write(serverDescription); ?>"><span class="glyphicon glyphicon-off"></span></button> + <button class="btn btn-default xbtn-sm" id="<? write(serverType); ?>ConfigureButton" data-toggle="tooltip" data-placement="bottom" title="Configure <? write(serverDescription); ?>"><span class="glyphicon glyphicon-wrench"></span></button> + <div class="checkbox pull-right"><label><input type="checkbox" id="<? write(serverType); ?>AutoScroll" checked="checked"> Autoscroll</label></div> + </div> + <div class="clearfix"></div> + <div class="webmonitor-scrolling-wrapper-with-buttons" id="<? write(serverType); ?>TableWrapper"> + <table class="table table-bordered table-condensed table-hover"> + <thead> + <tr> + <th>Date/Time</th> + <th>Level</th> + <th>Message</th> + </tr> + </thead> + <tbody id="<? write(serverType); ?>Log"> + </tbody> + </table> + </div> +</div> diff --git a/webv4/components/webmonitor/statistics-panel.xjs b/webv4/components/webmonitor/statistics-panel.xjs new file mode 100644 index 0000000000000000000000000000000000000000..b26007ba77e75913607358c62389d099d68addb4 --- /dev/null +++ b/webv4/components/webmonitor/statistics-panel.xjs @@ -0,0 +1,5 @@ +<div class="tab-pane fade webmonitor-panel <? write(argv[0]); ?>" id="statisticsTab"> + <div class="webmonitor-scrolling-wrapper"> + <h3>TODOX: Display statistics</h3> + </div> +</div> diff --git a/webv4/components/webmonitor/term-configuration-modal.xjs b/webv4/components/webmonitor/term-configuration-modal.xjs new file mode 100644 index 0000000000000000000000000000000000000000..bbc46d7a28f229541a851a1313bfd0a753bfdf76 --- /dev/null +++ b/webv4/components/webmonitor/term-configuration-modal.xjs @@ -0,0 +1,61 @@ +<div class="modal fade" id="termModal" tabindex="-1" role="dialog" aria-labelledby="termModalLabel"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title" id="termModalLabel">Terminal Server Configuration</h4> + </div> + <div class="modal-body" id="termModalLoading"> + <h1 align="center">Loading...</h1> + <!-- TODOZ Maybe a nice loading image? --> + </div> + <div class="modal-body" id="termModalBody" style="display: none;"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#termGeneralTab" data-toggle="tab">General</a></li> + <li><a href="#termTelnetTab" data-toggle="tab">Telnet</a></li> + <li><a href="#termRLoginTab" data-toggle="tab">RLogin</a></li> + <li><a href="#termSSHTab" data-toggle="tab">SSH</a></li> + <li><a href="#termSoundTab" data-toggle="tab">Sound</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane fade active in" id="termGeneralTab"> + <table class="table"> + <tr> + <td width="50%"> + <div>First Node<br /><input type="text" id="termFirstNode" class="form-control" value="1" style="width: 100px;" /></div> + <div>Last Node<br /><input type="text" id="termLastNode" class="form-control" value="4" style="width: 100px;" /></div> + <div>Max Concurrent Connections (0=no limit)<br /><input type="text" id="termMaxConcurrentConnections" class="form-control" value="N/A" style="width: 100px;" /></div> + </td> + <td width="50%"> + <div class="checkbox"><label><input type="checkbox" id="termAutoStart"> Auto Startup</label></div> + <div class="checkbox"><label><input type="checkbox" id="termXTRN_MINIMIZED"> Minimize Externals</label></div> + <div class="checkbox"><label><input type="checkbox" id="termYES_EVENTS"> Events Enabled</label></div> + <div class="checkbox disabled"><label><input type="checkbox" id="termEventsLogFile" disabled="disabled"> Log Events to Disk</label></div> + <div class="checkbox"><label><input type="checkbox" id="termYES_QWK_EVENTS"> QWK Message Events</label></div> + <div class="checkbox"><label><input type="checkbox" id="termYES_DOS"> DOS Program Support</label></div> + <div class="checkbox"><label><input type="checkbox" id="termYES_HOST_LOOKUP"> Hostname Lookup</label></div> + </td> + </tr> + </table> + </div> + <div class="tab-pane fade" id="termTelnetTab"> + <p>TODOX Telnet</p> + </div> + <div class="tab-pane fade" id="termRLoginTab"> + <p>TODOX RLogin</p> + </div> + <div class="tab-pane fade" id="termSSHTab"> + <p>TODOX SSH</p> + </div> + <div class="tab-pane fade" id="termSoundTab"> + <p>TODOX Sound</p> + </div> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-primary" id="termSaveChanges" style="display: none;">Save Changes</button> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/webv4/lib/auth.js b/webv4/lib/auth.js index bf1583886373f60d40ea0efea8f23b93e9a82517..6dda22957564148556826e73a998fbcb5cb23a4c 100644 --- a/webv4/lib/auth.js +++ b/webv4/lib/auth.js @@ -1,4 +1,5 @@ require('sbbsdefs.js', 'SYS_CLOSED'); +var request = require({}, settings.web_lib + 'request.js', 'request'); function randomString(length) { var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split(''); @@ -11,6 +12,14 @@ function randomString(length) { return str; } +function getCsrfToken() { + if (is_user()) { + return getSessionValue(user.number, 'csrf_token'); + } else { + return undefined; + } +} + function getSession(un) { var fn = format('%suser/%04d.web', system.data_dir, un); if (!file_exists(fn)) return false; @@ -48,6 +57,31 @@ function setCookie(usr, sessionKey) { } } +function validateCsrfToken() { + try { + // Check for CSRF token (in header or query data) + var input_token = null; + if (http_request.header['x-csrf-token']) { + input_token = http_request.header['x-csrf-token']; + } else if (request.has_param('csrf_token')) { + input_token = request.get_param('csrf_token'); + } + + // If we didn't find an input token, then validation fails + if (!input_token) { + return false; + } + + // If we did find an input token, confirm it matches the token stored in the user's session + return input_token === getCsrfToken(); + } catch (error) { + // In case of error, return false to avoid allowing CSRF when one shouldn't be allowed + log(LOG_ERR, 'auth.js validateCsrfToken error: ' + error); + return false; + } +} + + function validateSession(cookies) { var usr = new User(0); @@ -75,6 +109,12 @@ function validateSession(cookies) { setSessionValue(usr.number, 'ip_address', client.ip_address); if (session.session_start === undefined || time() - parseInt(session.session_start, 10) > settings.timeout) { setSessionValue(usr.number, 'session_start', time()); + + // Generate a csrf token. Minimum recommended is 128 bits of entropy, and 43 characters of 0-9A-Za-z should equal 256 bits of entropy + // according to this formula: log2(62^43) -- https://www.wolframalpha.com/input?i=log2%2862%5E43%29 + // (62 refers to the fact that there are 62 characters to choose from in the 0-9A-Za-z set) + setSessionValue(usr.number, 'csrf_token', randomString(43)) + if(!usr.is_sysop || (system.settings&SYS_SYSSTAT)) { load({}, 'logonlist_lib.js').add({ node: 'Web' }); } diff --git a/webv4/pages/More/001-nodespy.xjs b/webv4/pages/More/001-nodespy.xjs index fd85ec66142abe7948bdb8d5d6dd1e1e50bd13b9..23a197b9264b04c7685336bea52f00a65da0a7f3 100644 --- a/webv4/pages/More/001-nodespy.xjs +++ b/webv4/pages/More/001-nodespy.xjs @@ -1,13 +1,7 @@ <!--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(xjs_compile(settings.web_components + 'sysop-required.xjs')); // Load fTelnet-related files load(settings.web_lib + 'ftelnet.js'); @@ -36,7 +30,7 @@ <h1 align="center">Node Spy</h1> -<? loadComponent('mqtt.xjs'); ?> +<? load(xjs_compile(settings.web_components + 'mqtt.xjs')); ?> <!-- Multiple fTelnet instances (one per node) will be added to this wrapper div --> <div id="ClientsWrapper"></div> diff --git a/webv4/pages/More/001-webmonitor.xjs b/webv4/pages/More/001-webmonitor.xjs new file mode 100644 index 0000000000000000000000000000000000000000..f1912d646af40b7bb3da9ea0b63a1f5cd05934fd --- /dev/null +++ b/webv4/pages/More/001-webmonitor.xjs @@ -0,0 +1,62 @@ +<!--NO_SIDEBAR:Web Monitor--> + +<? + load(xjs_compile(settings.web_components + 'sysop-required.xjs')); + load(xjs_compile(settings.web_components + 'mqtt.xjs')); +?> + +<style> + div.webmonitor-panel { + height: 400px; + } + div.webmonitor-scrolling-wrapper { + height: 400px; + overflow-y: scroll; + } + div.webmonitor-scrolling-wrapper-with-buttons { + height: 356px; + overflow-y: scroll; + } + div.webmonitor-toolbar-buttons { + padding: 5px 0; + } + div.modal-body input[type=text] { + margin-bottom: 10px; + } + tr .visible-hover { + display: none; + } + tr:hover .visible-hover { + display: inline-block; + } +</style> + +<div id="webmonitor"> + <div class="row"> + <div class="col-md-6"> + <? load(xjs_compile(settings.web_components + 'webmonitor/quadrant.xjs'), 'top_left'); ?> + </div> + <div class="col-md-6"> + <? load(xjs_compile(settings.web_components + 'webmonitor/quadrant.xjs'), 'top_right'); ?> + </div> + </div> + <div class="row"> + <div class="col-md-6"> + <? load(xjs_compile(settings.web_components + 'webmonitor/quadrant.xjs'), 'bottom_left'); ?> + </div> + <div class="col-md-6"> + <? load(xjs_compile(settings.web_components + 'webmonitor/quadrant.xjs'), 'bottom_right'); ?> + </div> + </div> +</div> + +<? load(xjs_compile(settings.web_components + 'webmonitor/term-configuration-modal.xjs')); ?> + +<script> + // Set javascript variables for system variables referenced by webmonitor.js + var csrf_token = '<? write(getCsrfToken()); ?>'; + var webmonitor_configuration_enabled = <? write(settings.webmonitor_configuration_enabled); ?>; + var system_local_host_name = '<? write(system.local_host_name); ?>'; + var system_qwk_id = '<? write(system.qwk_id); ?>'; +</script> +<script src="./js/webmonitor.js"></script> \ No newline at end of file diff --git a/webv4/pages/More/webctrl.ini b/webv4/pages/More/webctrl.ini index 1db5cd48ef4d1bb234f879901dadf4da26d7388c..b558862628f2f5c6ab9ff1faeffe95ed8f36b6e7 100644 --- a/webv4/pages/More/webctrl.ini +++ b/webv4/pages/More/webctrl.ini @@ -7,3 +7,5 @@ AccessRequirements = level 90 [*userlist.xjs] AccessRequirements = LEVEL 50 AND REST NOT G +[*webmonitor.xjs] +AccessRequirements = level 90 diff --git a/webv4/root/api/webmonitor/get-term.ssjs b/webv4/root/api/webmonitor/get-term.ssjs new file mode 100644 index 0000000000000000000000000000000000000000..65206732b88447efba1ff745997427f953afc603 --- /dev/null +++ b/webv4/root/api/webmonitor/get-term.ssjs @@ -0,0 +1,39 @@ +require('sbbsdefs.js', 'SYS_CLOSED'); +var settings = load('modopts.js', 'web') || { web_directory: '../webv4' }; +load(settings.web_directory + '/lib/init.js'); +load(settings.web_lib + 'auth.js'); + +var reply = {}; + +function validateRequest() { + if (!user.is_sysop) { + reply = { error: 'You must be a SysOp to use this API' } + return false; + } else if (!validateCsrfToken()) { + reply = { error: 'Invalid or missing CSRF token' } + return false; + } + + return true; +} + +if (validateRequest()) { + // Read terminal server settings + var f = new File(system.ctrl_dir + 'sbbs.ini'); + if (f.open("r")) { + try { + reply = f.iniGetObject('BBS'); + } catch (error) { + reply = { error: error.message }; + } finally { + f.close(); + } + } else { + reply = { error: 'Unable to open sbbs.ini for reading' } + } +} + +reply = JSON.stringify(reply); +http_reply.header['Content-Type'] = 'application/json'; +http_reply.header['Content-Length'] = reply.length; +write(reply); diff --git a/webv4/root/api/webmonitor/update-term.ssjs b/webv4/root/api/webmonitor/update-term.ssjs new file mode 100644 index 0000000000000000000000000000000000000000..1bafd217867179cc4d41f94020a3dd005b7b6bfd --- /dev/null +++ b/webv4/root/api/webmonitor/update-term.ssjs @@ -0,0 +1,156 @@ +require('sbbsdefs.js', 'SYS_CLOSED'); +var settings = load('modopts.js', 'web') || { web_directory: '../webv4' }; +load(settings.web_directory + '/lib/init.js'); +load(settings.web_lib + 'auth.js'); +var request = require({}, settings.web_lib + 'request.js', 'request'); + +var reply = { errors: [], skipped: [], updated: [] }; + +function validateRequest() { + if (!user.is_sysop) { + reply.errors.push({ key: '', message: 'You must be a SysOp to use this API' }); + return false; + } else if (!validateCsrfToken()) { + reply.errors.push({ key: '', message: 'Invalid or missing CSRF token' }); + return false; + } + + return true; +} + +if (validateRequest()) { + // Update terminal server settings + var f = new File(system.ctrl_dir + 'sbbs.ini'); + if (f.open("r+")) { + try { + updateIntValue(f, 'FirstNode', 1, 255); + updateIntValue(f, 'LastNode', 1, 255); + updateIntValue(f, 'MaxConcurrentConnections', 0, 255); + updateBoolValue(f, 'AutoStart'); + updateOptions(f, 'XTRN_MINIMIZED'); + updateOptions(f, 'NO_EVENTS'); + updateOptions(f, 'NO_QWK_EVENTS'); + updateOptions(f, 'NO_DOS'); + updateOptions(f, 'NO_HOST_LOOKUP'); + } catch (error) { + reply.errors.push({ key: '', message: error.message }); + } finally { + f.close(); + } + } else { + reply.errors.push({ key: '', message: 'Unable to open sbbs.ini for writing' }); + } +} + +reply = JSON.stringify(reply); +http_reply.header['Content-Type'] = 'application/json'; +http_reply.header['Content-Length'] = reply.length; +write(reply); + + + +function updateBoolValue(f, key) { + // Check if parameter was sent in request + if (!request.has_param(key)) { + reply.errors.push({ key: key, message: 'Missing parameter' }); + return; + } + + // Check if parameter has a valid value + var re = /^(true|false)$/i; + if (!re.test(request.get_param(key))) { + reply.errors.push({ key: key, value: request.get_param(key), message: 'Value is not boolean' }); + return; + } + + // Skip if new value matches existing value + var newValue = JSON.parse(request.get_param(key).toLowerCase()); + if (newValue == f.iniGetValue('BBS', key, false)) { + reply.skipped.push({ key: key, value: request.get_param(key) }); + return; + } + + // Update the ini + f.iniSetValue('BBS', key, newValue); + reply.updated.push({ key: key, value: newValue }); +} + +function updateIntValue(f, key, min, max) { + // Check if parameter was sent in request + if (!request.has_param(key)) { + reply.errors.push({ key: key, message: 'Missing parameter' }); + return; + } + + // Check if parameter has a valid value + var re = /^\d+$/; + if (!re.test(request.get_param(key))) { + reply.errors.push({ key: key, value: request.get_param(key), message: 'Value is not numeric' }); + return; + } + + // Check if value is in allowed range + var newValue = parseInt(request.get_param(key), 10); + if ((newValue < min) || (newValue > max)) { + reply.errors.push({ key: key, value: request.get_param(key), message: 'Value is not in range (' + min + '-' + max + ')' }); + return; + } + + // Skip if new value matches existing value + if (newValue == f.iniGetValue('BBS', key, 0)) { + reply.skipped.push({ key: key, value: request.get_param(key) }); + return; + } + + // Update the ini + f.iniSetValue('BBS', key, newValue); + reply.updated.push({ key: key, value: newValue }); +} + +function updateOptions(f, option) { + // Check if parameter was sent in request + if (!request.has_param(option)) { + reply.errors.push({ key: option, message: 'Missing parameter' }); + return; + } + + // Check if option has a valid name + var re = /^[A-Z0-9_]+$/; + if (!re.test(option)) { + reply.errors.push({ key: option, value: request.get_param(option), message: 'Option is not valid' }); + return; + } + + // Check if parameter has a valid value + var re = /^(true|false)$/i; + if (!re.test(request.get_param(option))) { + reply.errors.push({ key: option, value: request.get_param(option), message: 'Value is not boolean' }); + return; + } + + // Read the existing options + var options = f.iniGetValue('BBS', 'Options', ''); + if (options) { + options = options.split(/\s*[|]\s*/); + } else { + options = []; + } + + // Skip if new value matches existing value + var enableOption = JSON.parse(request.get_param(option).toLowerCase()); + if (enableOption == options.includes(option)) { + reply.skipped.push({ key: option, value: request.get_param(option) }); + return; + } + + // Add or remove option based on value + if (enableOption) { + options.push(option); + } else { + options.splice(options.indexOf(option), 1); + } + + // Update the ini + f.iniSetValue('BBS', 'Options', options.join(' | ')); + reply.updated.push({ key: option, value: enableOption }); +} \ No newline at end of file diff --git a/webv4/root/js/mqtt.js b/webv4/root/js/mqtt.js index 51d77b3ffd866760a05da60f274482807dbe89a4..4d813eba7737f5caba890f8a70c32710745fb565 100644 --- a/webv4/root/js/mqtt.js +++ b/webv4/root/js/mqtt.js @@ -8,7 +8,7 @@ function mqtt_connect(topics, message_callback, log_callback) { username: broker_username, password: broker_password, protocolId: 'MQTT', - protocolVersion: 4, + protocolVersion: 5, clean: true, reconnectPeriod: 5000, connectTimeout: 30 * 1000, diff --git a/webv4/root/js/webmonitor.js b/webv4/root/js/webmonitor.js new file mode 100644 index 0000000000000000000000000000000000000000..bc36f366ef13592485d7d47a49999024a0b9e3a3 --- /dev/null +++ b/webv4/root/js/webmonitor.js @@ -0,0 +1,393 @@ +var client_lists = []; + +function handle_node_message(node, messageType, message) { + switch (messageType) { + case undefined: + // In “High” Publish Verbosity mode, human-readable node status messages are published directly to node/+ topics + // Example: sbbs/MYBBS/node/1 = Bubbaboy at external program menu via telnet + $('#nodeStatusLabel' + node).html(message); + break; + + case 'output': + // sbbs/+/node/+/output - live output to connected-terminal (for spying) + // Ignore, we have a separate page for node spy + break; + + case 'status': + // sbbs/+/node/+/status - tab-delimited node status values (see load/nodedefs.js for details) + // Example: sbbs/VERT/node/1/status = 0 0 1 65535 0 0 0 7 + // Ignore + break; + + case 'terminal': + // sbbs/+/node/+/terminal - tab-delimited current (or last) connected-terminal definition + // Example: sbbs/VERT/node/1/terminal = 80 24 syncterm ANSI CP437 6 0 2005 + // Ignore + break; + + default: + console.log('Unhandled node message: ' + JSON.stringify({node, messageType, message})); + break; + } +} + +function handle_resize() { + // Resize tabs to fit height of window + // 51 = navbar height, 20 = navbar margin bottom, 42 = top tab height, 42 = bottom tab height + var tabHeight = parseInt((window.innerHeight - 51 - 20 - 42 - 42) / 2, 10); + if (tabHeight < 200) { + tabHeight = 200; + } + $('div.webmonitor-panel').css('height', tabHeight + 'px'); + $('div.webmonitor-scrolling-wrapper').css('height', tabHeight + 'px'); + // 44 = height of buttons div + $('div.webmonitor-scrolling-wrapper-with-buttons').css('height', (tabHeight - 44) + 'px'); +} + +function handle_server_message(server, subtopic, subtopic2, message, packet) { + var ignoredSubtopics = ['error_count', 'served', 'state', 'version']; + var ignoredClientSubtopics = [undefined, 'action']; + + if (ignoredSubtopics.includes(subtopic)) { + // Ignore, not handling these subtopics at this time + return; + } else if ((subtopic === 'client') && ignoredClientSubtopics.includes(subtopic2)) { + // Ignore, not handling these client subtopics at this time + return; + } else if (subtopic === undefined) { + // sbbs/+/host/+/server/+ - tab-delimited status of each server is published to its server topic + // Examples: + // ready 8 served + // ready 32/150 clients 28 served + // DISCONNECTED + // Easier to handle here for all servers, instead of individually in the switch statement below + var status = message.split('\t')[0]; + switch (status.toLowerCase()) { + case 'disconnected': status = 'Stopped'; break; + case 'initializing': status = 'Starting'; break; + case 'ready': status = 'Running'; break; + case 'reloading': status = 'Restarting'; break; + case 'stopped': status = 'Stopped'; break; + case 'stopping': status = 'Stopping'; break; + default: console.log('Unknown Server Status: ' + status); break; + } + $('#' + server + 'StatusButton').text('Status: ' + status); + return; + } else if ((subtopic === 'client') && (subtopic2 === 'list')) { + // sbbs/+/host/+/server/+/client_list - tab-delimited details of all connected clients, one client per line + // Examples: + // 20230813T134846-240 Telnet 1 Ree ::1 localhost 54400 13184 + // 20230813T151452-240 Telnet 0 <unknown user> ::1 localhost 59258 18256 + // There is a timer set to fire every second, which will read client_lists[] and update the display, hence why + // nothing much is happening here + client_lists[server] = message; + return; + } else if (subtopic === 'log') { + // sbbs/+/host/+/server/+/log + // Each server/+ and event sub-topics has a log child topic where all messages of all log levels (severity) will be published as well as a grandchild topic for each log level (0-7, decreasing in severity) of logged messages. + // Easier to handle here for all servers, instead of individually in the switch statement below + if (subtopic2 !== undefined) { + var level = subtopic2; + try { + var dateTime = timestamp_to_date(packet.properties.userProperties.time).toLocaleString(); + } catch { + var dateTime = new Date().toLocaleString(); + } + $('#' + server + 'Log').append(`<tr class="${level_to_classname(level)}"><td style="white-space: nowrap;">${dateTime}</td><td align="center">${level_to_description(level)}</td><td>${message}</td></tr>`); + + if ($('#' + server + 'AutoScroll').is(':checked')) { + var tableWrapper = document.getElementById(server + 'TableWrapper'); + tableWrapper.scrollTop = tableWrapper.scrollHeight; + } + } + return; + } + + // If we get here, the message wasn't expected + console.log('Unhandled server->' + server + ' message: ' + JSON.stringify({server, subtopic, subtopic2, message})); +} + +function level_to_classname(level) { + switch (parseInt(level, 10)) { + case 0: return 'danger'; + case 1: return 'danger'; + case 2: return 'danger'; + case 3: return 'danger'; + case 4: return 'warning'; + case 5: return 'info'; + case 6: return ''; + case 7: return 'success'; + default: return 'active'; + } +} + +function level_to_description(level) { + switch (parseInt(level, 10)) { + case 0: return 'Emergency'; + case 1: return 'Alert'; + case 2: return 'Critical'; + case 3: return 'Error'; + case 4: return 'Warning'; + case 5: return 'Notice'; + case 6: return 'Normal'; + case 7: return 'Debug'; + default: return 'Unknown'; + } +} + +function mqtt_message(topic, message, packet) { + // Convert the message to a string and make it safe to display by replacing < with < + message = message.toString().replace(/</g, '<'); + + var arr = topic.split('/'); + if (arr[0] !== 'sbbs') { + console.log('ERROR: topic does not start with "sbbs": ' + JSON.stringify({topic, message})); + return; + } else if (arr[1] !== system_qwk_id) { + console.log('ERROR: topic does not start with BBSID (' + system_qwk_id + '): ' + JSON.stringify({topic, message})); + return; + } + + switch (arr[2]) { + case undefined: + // Ignore, message contains BBS Name + break; + + case 'action': + // Ignore, not handling anything under action at this time + break; + + case 'host': + // NB: arr[3] will be the host name + switch (arr[4]) { + case 'event': + switch (arr[5]) { + case undefined: + // Ignore, just says "thread started" + break; + + case 'log': + var level = arr[6]; + try { + var dateTime = timestamp_to_date(packet.properties.userProperties.time).toLocaleString(); + } catch { + var dateTime = new Date().toLocaleString(); + } + $('#eventsLog').append(`<tr class="${level_to_classname(level)}"><td style="white-space: nowrap;">${dateTime}</td><td align="center">${level_to_description(level)}</td><td>${message}</td></tr>`); + + var tab = document.getElementById('eventsTab'); + tab.scrollTop = tab.scrollHeight; + break; + + default: + console.log('ERROR: Unexpected topic element (5=' + arr[5] + '): ' + JSON.stringify({topic, message})); + break; + } + break; + + case 'server': + var server = arr[5]; + var subtopic = arr[6]; + var subtopic2 = arr[7]; + handle_server_message(server, subtopic, subtopic2, message, packet); + break; + + default: + console.log('ERROR: Unexpected topic element (4=' + arr[4] + '): ' + JSON.stringify({topic, message})); + break; + } + break; + + case 'node': + var node = arr[3]; + var messageType = arr[4]; + handle_node_message(node, messageType, message); + break; + + default: + console.log('ERROR: Unexpected topic element (2=' + arr[2] + '): ' + JSON.stringify({topic, message})); + break; + } +} + +function timestamp_to_date(timestamp) { + return new Date(timestamp.slice(0, 4), timestamp.slice(4, 6) - 1, timestamp.slice(6, 8), timestamp.slice(9, 11), timestamp.slice(11, 13), timestamp.slice(13, 15)) +} + +function update_clients() { + $('#clientsTable').empty(); + + // TODOY This displays clients in server order, might be nice to parse all the lists and sort by logonTime to show the longest-lived connections at the top + var totalClients = 0; + var servers = ['term', 'srvc', 'mail', 'ftp', 'web']; + for (var i = 0; i < servers.length; i++) { + if (client_lists[servers[i]]) { + var lines = client_lists[servers[i]].split('\n'); + for (var j = 0; j < lines.length; j++) { + // Examples: + // 20230813T134846-240 Telnet 1 Ree ::1 localhost 54400 13184 + // 20230813T151452-240 Telnet 0 <unknown user> ::1 localhost 59258 18256 + var arr = lines[j].split('\t'); + + var logonTime = timestamp_to_date(arr[0]); + var secondsElapsed = parseInt(((new Date()).getTime() - logonTime.getTime()) / 1000, 10); + var hoursElapsed = parseInt(Math.floor(secondsElapsed / 3600), 10); + secondsElapsed -= (hoursElapsed * 3600); + var minutesElapsed = parseInt(Math.floor(secondsElapsed / 60), 10); + secondsElapsed -= (minutesElapsed * 60); + + var protocol = arr[1]; + var username = arr[3]; + var address = arr[4]; + var hostname = arr[5]; + var port = arr[6]; + var socket = arr[7]; + + $('#clientsTable').append(`<tr><td>${socket}</td><td>${protocol}</td><td>${username}</td><td>${address}</td><td>${hostname}</td><td>${port}</td><td>${hoursElapsed}h ${minutesElapsed}m ${secondsElapsed}s</td></tr>`); + totalClients += 1; + } + } + } + + $('#clientsCounterBadge').html(totalClients.toString()); +} + +// Add an onload handler to setup the node spy +window.addEventListener('load', (event) => { + // Resize the panels to fit the window + handle_resize(); + + // Connect to mqtt broker and subscribe to a wildcard topic for this system + var topics = ['sbbs/' + system_qwk_id + '/#']; + mqtt_connect(topics, mqtt_message, console.log); + + // Set a 1 second timer to update the time display on the clients list (maybe for other purposes later too) + setInterval(function() { + update_clients(); + }, 1000); + + // Enable tooltips + $('[data-toggle="tooltip"]').tooltip() +}); + +window.addEventListener('resize', (event) => { + handle_resize(); +}); + +$('.webmonitor-recycle').click(function() { + var server = $(this).data('server'); + if (server) { + mqtt_publish('sbbs/' + system_qwk_id + '/host/' + system_local_host_name + '/server/' + server + '/recycle', 'recycle'); + } else { + mqtt_publish('sbbs/' + system_qwk_id + '/host/' + system_local_host_name + '/recycle', 'recycle'); + } +}); + +$('#ftpConfigureButton').click(function() { + if (!webmonitor_configuration_enabled) { + alert('Web Monitor Configuration is not enabled.\n\nSee the Documentation tab for more information.'); + return; + } + + alert('FTP Server Configuration is not implemented yet'); +}); + +$('#mailConfigureButton').click(function() { + if (!webmonitor_configuration_enabled) { + alert('Web Monitor Configuration is not enabled.\n\nSee the Documentation tab for more information.'); + return; + } + + alert('Mail Server Configuration is not implemented yet'); +}); + +$('#srvcConfigureButton').click(function() { + if (!webmonitor_configuration_enabled) { + alert('Web Monitor Configuration is not enabled.\n\nSee the Documentation tab for more information.'); + return; + } + + alert('Services Configuration is not implemented yet'); +}); + +$('#termConfigureButton').click(function() { + if (!webmonitor_configuration_enabled) { + alert('Web Monitor Configuration is not enabled.\n\nSee the Documentation tab for more information.'); + return; + } + + $('#termModal').modal('show'); + + $.ajax({ + url: './api/webmonitor/get-term.ssjs', + type: 'GET', + headers: { 'x-csrf-token': csrf_token } + }).done(function (data) { + if (data.error) { + alert(data.error); // TODOX Display on form instead of alert + $('#termModal').modal('hide'); + } else { + $('#termModalLoading').hide(); + $('#termModalBody').show(); + $('#termSaveChanges').show(); + + $('#termFirstNode').val(data.FirstNode); + $('#termLastNode').val(data.LastNode); + $('#termMaxConcurrentConnections').val(data.MaxConcurrentConnections); + $('#termAutoStart').prop('checked', data.AutoStart); + + var options = data.Options; + if (options) { + options = options.split(/\s*[|]\s*/) + } else { + options = []; + } + + $('#termXTRN_MINIMIZED').prop('checked', options.includes('XTRN_MINIMIZED')); + $('#termYES_EVENTS').prop('checked', !options.includes('NO_EVENTS')); + $('#termYES_QWK_EVENTS').prop('checked', !options.includes('NO_QWK_EVENTS')); + $('#termYES_DOS').prop('checked', !options.includes('NO_DOS')); + $('#termYES_HOST_LOOKUP').prop('checked', !options.includes('NO_HOST_LOOKUP')); + } + }); +}); + +$('#termSaveChanges').click(function() { + var requestModel = { + FirstNode: $('#termFirstNode').val(), + LastNode: $('#termLastNode').val(), + MaxConcurrentConnections: $('#termMaxConcurrentConnections').val(), + AutoStart: $('#termAutoStart').is(':checked'), + XTRN_MINIMIZED: $('#termXTRN_MINIMIZED').is(':checked'), + NO_EVENTS: !$('#termYES_EVENTS').is(':checked'), + NO_QWK_EVENTS: !$('#termYES_QWK_EVENTS').is(':checked'), + NO_DOS: !$('#termYES_DOS').is(':checked'), + NO_HOST_LOOKUP: !$('#termYES_HOST_LOOKUP').is(':checked'), + }; + + $.ajax({ + url: './api/webmonitor/update-term.ssjs', + type: 'POST', + data: requestModel, + headers: { 'x-csrf-token': csrf_token } + }).done(function (data) { + if (data.errors.length > 0) { + var message = ''; + for (var i = 0; i < data.errors.length; i++) { + message += data.errors[i].key + ' error: ' + data.errors[i].message + '\r\n'; + } + alert(message); // TODOX Display on form instead of alert + } else { + $('#termModal').modal('hide'); + } + }); +}); + +$('#webConfigureButton').click(function() { + if (!webmonitor_configuration_enabled) { + alert('Web Monitor Configuration is not enabled.\n\nSee the Documentation tab for more information.'); + return; + } + + alert('Web Server Configuration is not implemented yet'); +}); diff --git a/webv4/sidebar/002-recent-visitors.xjs b/webv4/sidebar/002-recent-visitors.xjs index 9c84161f7f0f2a38f276683c532c6d0530994cc7..be1869d34a2bcaa97203a81dea41d3ae3f763f54 100644 --- a/webv4/sidebar/002-recent-visitors.xjs +++ b/webv4/sidebar/002-recent-visitors.xjs @@ -13,7 +13,7 @@ <? ll.reverse().forEach(function (e) { ?> <li class="list-group-item striped"> <strong> - <? write(e.user.alias); ?> + <? write(e.user.alias.replace(/</g, '<')); ?> </strong> <br> <em> @@ -23,7 +23,7 @@ <? if (e.user.location != '') { ?> <? locale.write('label_location', 'sidebar_recent_visitors'); ?> <strong> - <? write(e.user.location); ?> + <? write(e.user.location.replace(/</g, '<')); ?> </strong> <? } ?> <? locale.write('label_connection', 'sidebar_recent_visitors'); ?>