Compare commits

...

6 commits

Author SHA1 Message Date
narspt 207898b578
Merge 2225f4e17f into 32c3241de0 2025-10-15 09:52:58 +03:00
LX1IQ 32c3241de0
Merge pull request #253 from MW0MWZ/master
Dashboard2 XSS / Security fixes
2025-10-14 13:43:36 +02:00
Andy Taylor 80821c25a3 Remove .DS_Store and update .gitignore 2025-10-14 12:26:32 +01:00
Andy Taylor 61204c3ed4 XSS Vulnerability Patches and Security Enhancements for Dashboard2 2025-10-14 12:25:26 +01:00
narspt 2225f4e17f copy xlxd.transcoder on make install 2023-12-03 19:02:34 +00:00
narspt 914de977a0 implement transcoder options and auto mode 2022-06-05 02:14:36 +01:00
23 changed files with 355 additions and 60 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
src/xlxd
ambed/ambed
ambedtest/ambedtest
.DS_Store

19
config/xlxd.transcoder Normal file
View file

@ -0,0 +1,19 @@
#########################################################################################
# XLXD transcoder options file
#
# One line per entry, each entry specifies a transcoder option.
#
# Valid options (case-sensitive):
# Address <ip> - Ip address of the ambed server used for transcoding.
# ModulesOn <modules> - A string with all modules which transcoding should be enabled,
# "*" means all modules.
# ModulesAuto <modules> - A string with all modules which transcoding should be enabled
# in automatic mode (transcoding will get enabled only if there
# are clients linked on the module with different codecs),
# "*" means all modules. ModulesOn supersedes ModulesAuto.
#
#########################################################################################
Address 127.0.0.1
ModulesOn ABCD
ModulesAuto *

View file

@ -23,6 +23,7 @@ $PageOptions['PageRefreshActive'] = true; // Activate automa
$PageOptions['PageRefreshDelay'] = '10000'; // Page refresh time in miliseconds
$PageOptions['NumberOfModules'] = 10; // Number of Modules enabled on reflector
$PageOptions['TranscoderFile'] = '/xlxd/xlxd.transcoder'; // Path to transcoder file
$PageOptions['RepeatersPage'] = array();
$PageOptions['RepeatersPage']['LimitTo'] = 99; // Number of Repeaters to show

View file

@ -1,8 +1,9 @@
<table class="listingtable">
<tr>
<th width="80" rowspan="2">Module</th>
<th width="75" rowspan="2">Module</th>
<th width="130" rowspan="2">Name</th>
<th width="65" rowspan="2">Users</th>
<th width="60" rowspan="2">Users</th>
<th width="60" rowspan="2">Trans<br />coder</th>
<th colspan="2">DPlus</th>
<th colspan="2">DExtra</th>
<th colspan="2">DCS</th>
@ -11,17 +12,35 @@
</tr>
<tr>
<th width="100">URCALL</th>
<th width="100">DTMF</th>
<th width="85">DTMF</th>
<th width="100">URCALL</th>
<th width="100">DTMF</th>
<th width="85">DTMF</th>
<th width="100">URCALL</th>
<th width="100">DTMF</th>
<th width="85">DTMF</th>
</tr>
<?php
$ReflectorNumber = substr($Reflector->GetReflectorName(), 3, 3);
$NumberOfModules = isset($PageOptions['NumberOfModules']) ? min(max($PageOptions['NumberOfModules'],0),26) : 26;
$TranscoderModulesOn = '';
$TranscoderModulesAuto = '';
if (isset($PageOptions['TranscoderFile']) && file_exists($PageOptions['TranscoderFile']) && is_readable($PageOptions['TranscoderFile'])) {
$TranscoderFileContent = file($PageOptions['TranscoderFile']);
for ($i=0; $i < count($TranscoderFileContent); $i++) {
if (substr(trim($TranscoderFileContent[$i]), 0, 1) != '#') {
$TranscoderOption = explode(" ", trim($TranscoderFileContent[$i]));
if (isset($TranscoderOption[0]) && isset($TranscoderOption[1])) {
if ($TranscoderOption[0] === 'ModulesOn') {
$TranscoderModulesOn = trim($TranscoderOption[1]);
} else if ($TranscoderOption[0] === 'ModulesAuto') {
$TranscoderModulesAuto = trim($TranscoderOption[1]);
}
}
}
}
}
$odd = "";
for ($i = 1; $i <= $NumberOfModules; $i++) {
@ -30,11 +49,19 @@ for ($i = 1; $i <= $NumberOfModules; $i++) {
if ($odd == "#FFFFFF") { $odd = "#F1FAFA"; } else { $odd = "#FFFFFF"; }
$transcoderstate = 'Off';
if ((strstr($TranscoderModulesOn,'*') !== false) || (strstr($TranscoderModulesOn,$module) !== false)) {
$transcoderstate = 'On';
} else if ((strstr($TranscoderModulesAuto,'*') !== false) || (strstr($TranscoderModulesAuto,$module) !== false)) {
$transcoderstate = 'Auto';
}
echo '
<tr height="30" bgcolor="'.$odd.'" onMouseOver="this.bgColor=\'#FFFFCA\';" onMouseOut="this.bgColor=\''.$odd.'\';">
<td align="center">'. $module .'</td>
<td align="center">'. (empty($PageOptions['ModuleNames'][$module]) ? '-' : $PageOptions['ModuleNames'][$module]) .'</td>
<td align="center">'. count($Reflector->GetNodesInModulesByID($module)) .'</td>
<td align="center">'. $transcoderstate .'</td>
<td align="center">'. 'REF' . $ReflectorNumber . $module . 'L' .'</td>
<td align="center">'. (is_numeric($ReflectorNumber) ? '*' . sprintf('%01d',$ReflectorNumber) . (($i<=4)?$module:sprintf('%02d',$i)) : '-') .'</td>
<td align="center">'. 'XRF' . $ReflectorNumber . $module . 'L' .'</td>

View file

@ -1,3 +1,28 @@
xlx db v2.3.8
SECURITY UPDATE - XSS Vulnerability Patches and Security Enhancements
- "functions.php" added SafeOutput() and SafeOutputAttr() for XSS protection
added GenerateCSRFToken() and ValidateCSRFToken() for CSRF protection
- "index.php" added session_start() for CSRF token support
added SafeOutput() to all $_GET['show'] outputs
added input whitelist validation for $_GET['show'] parameter
changed file permission from 777 to 600 for hash file (security hardening)
added SafeOutputAttr() to all meta tag outputs
added SafeOutput() to contact email output
improved error messages to prevent information disclosure
- "users.php" added CSRF token validation for all POST requests
added CSRF tokens to both filter forms
added input validation with regex for callsign filter (alphanumeric, dash, asterisk only)
added input validation with regex for module filter (single letter A-Z only)
added SafeOutput() and SafeOutputAttr() to all user data outputs
added SafeOutput() to all callsign, suffix, via, peer, and module outputs
- "repeaters.php" added SafeOutput() to all node callsign, suffix, protocol, and module outputs
added SafeOutput() to all IP address outputs
- "peers.php" added SafeOutput() and SafeOutputAttr() to peer name and URL outputs
added SafeOutput() to protocol, module, and IP address outputs
- "reflectors.php" added SafeOutput() and SafeOutputAttr() to reflector name, country, comment, and URL outputs
- "class.reflector.php" added URL validation in CallHome() method to prevent remote file inclusion attacks
xlx db v2.3.1
- "config.inc.php" $CallingHome['InterlinkFile'] added
@ -16,7 +41,7 @@ xlx db v2.2.2
This version is a major release with voluntary self-registration feature build in.
You need to edit the conf.inc.php to your needs.
On the first run your personal hash to access the database is place in the servers /tmp folder.
On the first run your personal hash to access the database is place in the server<EFBFBD>s /tmp folder.
Take care to make a backup of this file because this folder is cleaned up after a server reboot.
This version is a major release
@ -44,7 +69,7 @@ xlx db v2.1.4
- "class.reflector.php" improved the flag search
- "country.csv" added serveral prefixes
- "flags" added Puerto Ricco and Åland Islands
- "flags" added Puerto Ricco and <EFBFBD>land Islands
xlx db v2.1.3

View file

@ -1,4 +1,5 @@
<?php
session_start();
/*
* This dashboard is being developed by the DVBrazil Team as a courtesy to
@ -9,12 +10,12 @@
if (file_exists("./pgs/functions.php")) {
require_once("./pgs/functions.php");
} else {
die("functions.php does not exist.");
die("Required file not found.");
}
if (file_exists("./pgs/config.inc.php")) {
require_once("./pgs/config.inc.php");
} else {
die("config.inc.php does not exist.");
die("Required file not found.");
}
if (!class_exists('ParseXML')) require_once("./pgs/class.parsexml.php");
@ -44,7 +45,7 @@ if ($CallingHome['Active']) {
@fwrite($Ressource, "\n" . '$Hash = "' . $Hash . '";');
@fwrite($Ressource, "\n\n" . '?>');
@fclose($Ressource);
@exec("chmod 777 " . $CallingHome['HashFile']);
@exec("chmod 600 " . $CallingHome['HashFile']);
$CallHomeNow = true;
}
} else {
@ -79,12 +80,11 @@ if ($CallingHome['Active']) {
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="<?php echo $PageOptions['MetaDescription']; ?>"/>
<meta name="keywords" content="<?php echo $PageOptions['MetaKeywords']; ?>"/>
<meta name="author" content="<?php echo $PageOptions['MetaAuthor']; ?>"/>
<meta name="revisit" content="<?php echo $PageOptions['MetaRevisit']; ?>"/>
<meta name="robots" content="<?php echo $PageOptions['MetaAuthor']; ?>"/>
<meta name="description" content="<?php echo SafeOutputAttr($PageOptions['MetaDescription']); ?>"/>
<meta name="keywords" content="<?php echo SafeOutputAttr($PageOptions['MetaKeywords']); ?>"/>
<meta name="author" content="<?php echo SafeOutputAttr($PageOptions['MetaAuthor']); ?>"/>
<meta name="revisit" content="<?php echo SafeOutputAttr($PageOptions['MetaRevisit']); ?>"/>
<meta name="robots" content="<?php echo SafeOutputAttr($PageOptions['MetaAuthor']); ?>"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title><?php echo $Reflector->GetReflectorName(); ?> Reflector Dashboard</title>
<link rel="icon" href="./favicon.ico" type="image/vnd.microsoft.icon">
@ -113,8 +113,8 @@ if ($CallingHome['Active']) {
if (($_SERVER['REQUEST_METHOD'] === 'POST') || isset($_GET['do'])) {
echo '
document.location.href = "./index.php';
if (isset($_GET['show'])) {
echo '?show=' . $_GET['show'];
if (isset($_GET['show']) && $_GET['show'] !== '') {
echo '?show=' . SafeOutput($_GET['show']);
}
echo '";';
} else {
@ -194,6 +194,15 @@ if ($CallingHome['Active']) {
}
}
// Whitelist allowed values
if (!isset($_GET['show'])) {
$_GET['show'] = '';
}
$allowed_shows = ['users', 'repeaters', 'liveircddb', 'peers', 'reflectors', ''];
if (!in_array($_GET['show'], $allowed_shows, true)) {
$_GET['show'] = '';
}
switch ($_GET['show']) {
case 'users' :
require_once("./pgs/users.php");
@ -222,7 +231,7 @@ if ($CallingHome['Active']) {
<footer class="footer">
<div class="container">
<p><a href="mailto:<?php echo $PageOptions['ContactEmail']; ?>"><?php echo $PageOptions['ContactEmail']; ?></a>
<p><a href="mailto:<?php echo SafeOutputAttr($PageOptions['ContactEmail']); ?>"><?php echo SafeOutput($PageOptions['ContactEmail']); ?></a>
</p>
</div>
</footer>

View file

@ -419,6 +419,10 @@ class xReflector {
}
public function CallHome() {
// Validate URL before making request
if (!filter_var($this->CallingHomeServerURL, FILTER_VALIDATE_URL)) {
return false;
}
$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<query>CallingHome</query>'.$this->ReflectorXML.$this->InterlinkXML;
$p = @stream_context_create(array('http' => array('header' => "Content-type: application/x-www-form-urlencoded\r\n",

View file

@ -16,7 +16,7 @@ $PageOptions = array();
$PageOptions['ContactEmail'] = 'your_email'; // Support E-Mail address
$PageOptions['DashboardVersion'] = '2.3.7'; // Dashboard Version
$PageOptions['DashboardVersion'] = '2.3.8'; // Dashboard Version
$PageOptions['PageRefreshActive'] = true; // Activate automatic refresh
$PageOptions['PageRefreshDelay'] = '10000'; // Page refresh time in miliseconds

0
dashboard2/pgs/country.csv Executable file → Normal file
View file

View file

@ -59,4 +59,30 @@ function CreateCode ($laenge) {
return $out;
}
function SafeOutput($string, $encoding = 'UTF-8') {
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, $encoding);
}
function SafeOutputAttr($string, $encoding = 'UTF-8') {
// Extra safe for attributes
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, $encoding);
}
function GenerateCSRFToken() {
if (!isset($_SESSION)) {
session_start();
}
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function ValidateCSRFToken($token) {
if (!isset($_SESSION)) {
session_start();
}
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
?>

View file

@ -44,8 +44,8 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) {
<tr class="table-center">
<td>'.($i+1).'</td>';
$Name = $Reflector->Peers[$i]->GetCallSign();
$URL = '';
$Name = $Reflector->Peers[$i]->GetCallSign();
$URL = '';
for ($j=1;$j<count($Reflectors);$j++) {
if ($Name === $XML->GetElement($Reflectors[$j], "name")) {
@ -53,15 +53,15 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) {
}
}
if ($Result && (trim($URL) != "")) {
echo '<td><a href="'.$URL.'" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;'.$Name.'" style="text-decoration:none;color:#000000;">'.$Name.'</a></td>';
echo '<td><a href="' . SafeOutputAttr($URL) . '" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;' . SafeOutputAttr($Name) . '" style="text-decoration:none;color:#000000;">' . SafeOutput($Name) . '</a></td>';
} else {
echo '<td>'.$Name.'</td>';
echo '<td>' . SafeOutput($Name) . '</td>';
}
echo '
<td>'.date("d.m.Y H:i", $Reflector->Peers[$i]->GetLastHeardTime()).'</td>
<td>'.FormatSeconds(time()-$Reflector->Peers[$i]->GetConnectTime()).' s</td>
<td>'.$Reflector->Peers[$i]->GetProtocol().'</td>
<td>'.$Reflector->Peers[$i]->GetLinkedModule().'</td>';
<td>'.SafeOutput($Reflector->Peers[$i]->GetProtocol()).'</td>
<td>'.SafeOutput($Reflector->Peers[$i]->GetLinkedModule()).'</td>';
if ($PageOptions['PeerPage']['IPModus'] != 'HideIP') {
echo '<td>';
$Bytes = explode(".", $Reflector->Peers[$i]->GetIP());
@ -70,7 +70,7 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) {
case 'ShowLast1ByteOfIP' : echo $PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$Bytes[3]; break;
case 'ShowLast2ByteOfIP' : echo $PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$Bytes[2].'.'.$Bytes[3]; break;
case 'ShowLast3ByteOfIP' : echo $PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$Bytes[1].'.'.$Bytes[2].'.'.$Bytes[3]; break;
default : echo '<a href="http://'.$Reflector->Peers[$i]->GetIP().'" target="_blank" style="text-decoration:none;color:#000000;">'.$Reflector->Peers[$i]->GetIP().'</a>';
default : echo '<a href="http://'.SafeOutput($Reflector->Peers[$i]->GetIP()).'" target="_blank" style="text-decoration:none;color:#000000;">'.SafeOutput($Reflector->Peers[$i]->GetIP()).'</a>';
}
}
echo '</td>';

View file

@ -38,10 +38,10 @@ for ($i=0;$i<count($Reflectors);$i++) {
echo '
<tr class="table-center">
<td>'.($i+1).'</td>
<td><a href="'.$DASHBOARDURL.'" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;'.$NAME.'">'.$NAME.'</a></td>
<td>'.$COUNTRY.'</td>
<td><a href="' . SafeOutputAttr($DASHBOARDURL) . '" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;' . SafeOutputAttr($NAME) . '">' . SafeOutput($NAME) . '</a></td>
<td>' . SafeOutput($COUNTRY) . '</td>
<td><img src="./img/'; if ($LASTCONTACT<(time()-1800)) { echo 'down'; } ELSE { echo 'up'; } echo '.png" class="table-status" alt=""></td>
<td>'.$COMMENT.'</td>
<td>' . SafeOutput($COMMENT) . '</td>
</tr>';
}

View file

@ -30,10 +30,10 @@ for ($i=0;$i<$Reflector->NodeCount();$i++) {
echo '<a href="#" class="tip"><img src="./img/flags/'.$Flag.'.png" class="table-flag" alt="'.$Name.'"><span>'.$Name.'</span></a>';
}
echo '</td>
<td><a href="http://www.aprs.fi/'.$Reflector->Nodes[$i]->GetCallSign();
if ($Reflector->Nodes[$i]->GetSuffix() != "") echo '-'.$Reflector->Nodes[$i]->GetSuffix();
echo '" class="pl" target="_blank">'.$Reflector->Nodes[$i]->GetCallSign();
if ($Reflector->Nodes[$i]->GetSuffix() != "") { echo '-'.$Reflector->Nodes[$i]->GetSuffix(); }
<td><a href="http://www.aprs.fi/'.SafeOutput($Reflector->Nodes[$i]->GetCallSign());
if ($Reflector->Nodes[$i]->GetSuffix() != "") echo '-'.SafeOutput($Reflector->Nodes[$i]->GetSuffix());
echo '" class="pl" target="_blank">'.SafeOutput($Reflector->Nodes[$i]->GetCallSign());
if ($Reflector->Nodes[$i]->GetSuffix() != "") { echo '-'.SafeOutput($Reflector->Nodes[$i]->GetSuffix()); }
echo '</a></td>
<td>';
if (($Reflector->Nodes[$i]->GetPrefix() == 'REF') || ($Reflector->Nodes[$i]->GetPrefix() == 'XRF')) {
@ -55,8 +55,8 @@ for ($i=0;$i<$Reflector->NodeCount();$i++) {
echo '</td>
<td>'.date("d.m.Y H:i", $Reflector->Nodes[$i]->GetLastHeardTime()).'</td>
<td>'.FormatSeconds(time()-$Reflector->Nodes[$i]->GetConnectTime()).' s</td>
<td>'.$Reflector->Nodes[$i]->GetProtocol().'</td>
<td>'.$Reflector->Nodes[$i]->GetLinkedModule().'</td>';
<td>'.SafeOutput($Reflector->Nodes[$i]->GetProtocol()).'</td>
<td>'.SafeOutput($Reflector->Nodes[$i]->GetLinkedModule()).'</td>';
if ($PageOptions['RepeatersPage']['IPModus'] != 'HideIP') {
echo '
<td>';

View file

@ -9,6 +9,11 @@ if (!isset($_SESSION['FilterModule'])) {
}
if (isset($_POST['do'])) {
// Validate CSRF token
if (!isset($_POST['csrf_token']) || !ValidateCSRFToken($_POST['csrf_token'])) {
die('CSRF token validation failed');
}
if ($_POST['do'] == 'SetFilter') {
if (isset($_POST['txtSetCallsignFilter'])) {
@ -17,12 +22,17 @@ if (isset($_POST['do'])) {
$_SESSION['FilterCallSign'] = null;
}
else {
$_SESSION['FilterCallSign'] = $_POST['txtSetCallsignFilter'];
if (strpos($_SESSION['FilterCallSign'], "*") === false) {
$_SESSION['FilterCallSign'] = "*".$_SESSION['FilterCallSign']."*";
// Validate callsign format (alphanumeric, dash, asterisk only)
if (preg_match('/^[A-Z0-9\-\*]+$/i', $_POST['txtSetCallsignFilter'])) {
$_SESSION['FilterCallSign'] = $_POST['txtSetCallsignFilter'];
if (strpos($_SESSION['FilterCallSign'], "*") === false) {
$_SESSION['FilterCallSign'] = "*".$_SESSION['FilterCallSign']."*";
}
} else {
// Invalid format, reject it
$_SESSION['FilterCallSign'] = null;
}
}
}
if (isset($_POST['txtSetModuleFilter'])) {
@ -31,9 +41,14 @@ if (isset($_POST['do'])) {
$_SESSION['FilterModule'] = null;
}
else {
$_SESSION['FilterModule'] = $_POST['txtSetModuleFilter'];
// Validate module is single letter A-Z
if (preg_match('/^[A-Z]$/i', $_POST['txtSetModuleFilter'])) {
$_SESSION['FilterModule'] = strtoupper($_POST['txtSetModuleFilter']);
} else {
// Invalid format, reject it
$_SESSION['FilterModule'] = null;
}
}
}
}
}
@ -60,7 +75,8 @@ if ($PageOptions['UserPage']['ShowFilter']) {
<td align="left">
<form name="frmFilterCallSign" method="post" action="./index.php">
<input type="hidden" name="do" value="SetFilter" />
<input type="text" class="FilterField" value="'.$_SESSION['FilterCallSign'].'" name="txtSetCallsignFilter" placeholder="Callsign" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="hidden" name="csrf_token" value="' . GenerateCSRFToken() . '" />
<input type="text" class="FilterField" value="'.SafeOutputAttr($_SESSION['FilterCallSign']).'" name="txtSetCallsignFilter" placeholder="Callsign" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="submit" value="Apply" class="FilterSubmit" />
</form>
</td>';
@ -72,7 +88,8 @@ if ($PageOptions['UserPage']['ShowFilter']) {
<td align="right" style="padding-right:3px;">
<form name="frmFilterModule" method="post" action="./index.php">
<input type="hidden" name="do" value="SetFilter" />
<input type="text" class="FilterField" value="'.$_SESSION['FilterModule'].'" name="txtSetModuleFilter" placeholder="Module" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="hidden" name="csrf_token" value="' . GenerateCSRFToken() . '" />
<input type="text" class="FilterField" value="'.SafeOutputAttr($_SESSION['FilterModule']).'" name="txtSetModuleFilter" placeholder="Module" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="submit" value="Apply" class="FilterSubmit" />
</form>
</td>
@ -133,16 +150,16 @@ for ($i=0;$i<$Reflector->StationCount();$i++) {
echo '<a href="#" class="tip"><img src="./img/flags/' . $Flag . '.png" class="table-flag" alt="' . $Name . '"><span>' . $Name . '</span></a>';
}
echo '</td>
<td><a href="https://www.qrz.com/db/' . $Reflector->Stations[$i]->GetCallsignOnly() . '" class="pl" target="_blank">' . $Reflector->Stations[$i]->GetCallsignOnly() . '</a></td>
<td>' . $Reflector->Stations[$i]->GetSuffix() . '</td>
<td><a href="http://www.aprs.fi/' . $Reflector->Stations[$i]->GetCallsignOnly() . '" class="pl" target="_blank"><img src="./img/sat.png" alt=""></a></td>
<td>' . $Reflector->Stations[$i]->GetVia();
<td><a href="https://www.qrz.com/db/' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . '" class="pl" target="_blank">' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . '</a></td>
<td>' . SafeOutput($Reflector->Stations[$i]->GetSuffix()) . '</td>
<td><a href="http://www.aprs.fi/' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . '" class="pl" target="_blank"><img src="./img/sat.png" alt=""></a></td>
<td>' . SafeOutput($Reflector->Stations[$i]->GetVia());
if ($Reflector->Stations[$i]->GetPeer() != $Reflector->GetReflectorName()) {
echo ' / ' . $Reflector->Stations[$i]->GetPeer();
echo ' / ' . SafeOutput($Reflector->Stations[$i]->GetPeer());
}
echo '</td>
<td>' . @date("d.m.Y H:i", $Reflector->Stations[$i]->GetLastHeardTime()) . '</td>
<td>' . $Reflector->Stations[$i]->GetModule() . '</td>
<td>' . SafeOutput($Reflector->Stations[$i]->GetModule()) . '</td>
</tr>';
}
if ($i == $PageOptions['LastHeardPage']['LimitTo']) {
@ -192,7 +209,7 @@ for ($i=0;$i<count($Modules);$i++) {
$Displayname = $Reflector->GetCallsignAndSuffixByID($Users[$j]);
echo '
<tr>
<td><a href="http://www.aprs.fi/'.$Displayname.'" class="pl" target="_blank">'.$Displayname.'</a> </td>
<td><a href="http://www.aprs.fi/' . SafeOutput($Displayname) . '" class="pl" target="_blank">' . SafeOutput($Displayname) . '</a> </td>
</tr>';
$UserCheckedArray[] = $Users[$j];
}

View file

@ -19,7 +19,7 @@ PATH=/sbin:/bin:/usr/sbin:/usr/bin
# change below settings according to your system
NAME="xlxd"
DAEMON="/xlxd/xlxd"
ARGUMENTS="XLX999 192.168.1.240 127.0.0.1"
ARGUMENTS="XLX999 192.168.1.240"
PIDFILE="/var/log/xlxd.pid"
USER=root
GROUP=root

View file

@ -40,7 +40,7 @@ CPacketStream::CPacketStream()
////////////////////////////////////////////////////////////////////////////////////////
// open / close
bool CPacketStream::Open(const CDvHeaderPacket &DvHeader, CClient *client)
bool CPacketStream::Open(const CDvHeaderPacket &DvHeader, CClient *client, bool enableTranscoding)
{
bool ok = false;
@ -54,7 +54,11 @@ bool CPacketStream::Open(const CDvHeaderPacket &DvHeader, CClient *client)
m_DvHeader = DvHeader;
m_OwnerClient = client;
m_LastPacketTime.Now();
m_CodecStream = g_Transcoder.GetStream(this, client->GetCodec());
if (enableTranscoding) {
m_CodecStream = g_Transcoder.GetStream(this, client->GetCodec());
} else {
m_CodecStream = g_Transcoder.GetStream(this, CODEC_NONE);
}
ok = true;
}
return ok;

View file

@ -48,7 +48,7 @@ public:
virtual ~CPacketStream() {};
// open / close
bool Open(const CDvHeaderPacket &, CClient *);
bool Open(const CDvHeaderPacket &, CClient *, bool enableTranscoding);
void Close(void);
// push & pop

View file

@ -214,13 +214,36 @@ CPacketStream *CReflector::OpenStream(CDvHeaderPacket *DvHeader, CClient *client
{
// get the module's queue
char module = DvHeader->GetRpt2Module();
bool enableTranscoding = false;
if (g_Transcoder.IsModuleOn(module))
{
enableTranscoding = true;
}
else if (g_Transcoder.IsModuleAuto(module))
{
// enable transcoding only if we have clients on the module with different codecs
uint8 clientCodecs = 0;
for ( int i = 0; i < m_Clients.GetSize(); i++ )
{
if ( m_Clients.GetClient(i)->GetReflectorModule() == module )
{
clientCodecs |= m_Clients.GetClient(i)->GetCodec();
if ( clientCodecs == (CODEC_AMBEPLUS | CODEC_AMBE2PLUS) ) {
enableTranscoding = true;
break;
}
}
}
}
CPacketStream *stream = GetStream(module);
if ( stream != NULL )
{
// lock it
stream->Lock();
// is it available ?
if ( stream->Open(*DvHeader, client) )
if ( stream->Open(*DvHeader, client, enableTranscoding) )
{
// stream open, mark client as master
// so that it can't be deleted

View file

@ -22,6 +22,8 @@
// along with Foobar. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#include <string.h>
#include <sys/stat.h>
#include "main.h"
#include "creflector.h"
#include "ctranscoder.h"
@ -55,6 +57,10 @@ CTranscoder::CTranscoder()
m_bStreamOpened = false;
m_StreamidOpenStream = 0;
m_PortOpenStream = 0;
m_ModulesOn = "*"; // default if xlxd.transcoder does not exist
m_ModulesAuto = "";
m_LastNeedReloadTime.Now();
m_LastModTime = 0;
}
////////////////////////////////////////////////////////////////////////////////////////
@ -90,6 +96,8 @@ bool CTranscoder::Init(void)
{
bool ok;
ReadOptions();
// reset stop flag
m_bStopThread = false;
@ -197,6 +205,16 @@ void CTranscoder::Task(void)
// update time
m_LastKeepaliveTime.Now();
}
// check if options need reload every 30 seconds
if ( m_LastNeedReloadTime.DurationSinceNow() > 30 )
{
// reload options if needed
NeedReload();
// update time
m_LastNeedReloadTime.Now();
}
}
////////////////////////////////////////////////////////////////////////////////////////
@ -400,3 +418,105 @@ void CTranscoder::EncodeClosestreamPacket(CBuffer *Buffer, uint16 uiStreamId)
Buffer->Append((uint16)uiStreamId);
}
////////////////////////////////////////////////////////////////////////////////////////
// options helpers
char *CTranscoder::TrimWhiteSpaces(char *str)
{
char *end;
while ((*str == ' ') || (*str == '\t')) str++;
if (*str == 0)
return str;
end = str + strlen(str) - 1;
while ((end > str) && ((*end == ' ') || (*end == '\t') || (*end == '\r'))) end --;
*(end + 1) = 0;
return str;
}
void CTranscoder::NeedReload(void)
{
struct stat fileStat;
if (::stat(TRANSCODEROPTIONS_PATH, &fileStat) != -1)
{
if (m_LastModTime != fileStat.st_mtime)
{
ReadOptions();
}
}
}
void CTranscoder::ReadOptions(void)
{
char sz[256];
int opts = 0;
std::ifstream file(TRANSCODEROPTIONS_PATH);
if (file.is_open())
{
m_ModulesOn = "";
m_ModulesAuto = "";
while (file.getline(sz, sizeof(sz)).good())
{
char *szt = TrimWhiteSpaces(sz);
char *szval;
if ((::strlen(szt) > 0) && szt[0] != '#')
{
if ((szt = ::strtok(szt, " ,\t")) != NULL)
{
if ((szval = ::strtok(NULL, " ,\t")) != NULL)
{
if (::strncmp(szt, "Address", 7) == 0)
{
CIp Ip = CIp(szval);
if (Ip.GetAddr())
{
std::cout << "Transcoder Address set to " << Ip << std::endl;
g_Reflector.SetTranscoderIp(Ip);
m_Ip = Ip;
opts++;
}
}
else if (strncmp(szt, "ModulesOn", 9) == 0)
{
std::cout << "Transcoder ModulesOn set to " << szval << std::endl;
m_ModulesOn = szval;
opts++;
}
else if (strncmp(szt, "ModulesAuto", 11) == 0)
{
std::cout << "Transcoder ModulesAuto set to " << szval << std::endl;
m_ModulesAuto = szval;
opts++;
}
else
{
// unknown option - ignore
}
}
}
}
}
std::cout << "Transcoder loaded " << opts << " options from file " << TRANSCODEROPTIONS_PATH << std::endl;
file.close();
struct stat fileStat;
if (::stat(TRANSCODEROPTIONS_PATH, &fileStat) != -1)
{
m_LastModTime = fileStat.st_mtime;
}
}
}
bool CTranscoder::IsModuleOn(char module)
{
return ( strchr(m_ModulesOn.c_str(), '*') || strchr(m_ModulesOn.c_str(), module) );
}
bool CTranscoder::IsModuleAuto(char module)
{
return ( strchr(m_ModulesAuto.c_str(), '*') || strchr(m_ModulesAuto.c_str(), module) );
}

View file

@ -66,6 +66,10 @@ public:
static void Thread(CTranscoder *);
void Task(void);
// options
bool IsModuleOn(char);
bool IsModuleAuto(char);
protected:
// keepalive helpers
void HandleKeepalives(void);
@ -80,6 +84,11 @@ protected:
void EncodeOpenstreamPacket(CBuffer *, uint8, uint8);
void EncodeClosestreamPacket(CBuffer *, uint16);
// options
char *TrimWhiteSpaces(char *);
void NeedReload(void);
void ReadOptions(void);
protected:
// streams
std::mutex m_Mutex;
@ -103,6 +112,12 @@ protected:
// time
CTimePoint m_LastKeepaliveTime;
CTimePoint m_LastActivityTime;
// options
std::string m_ModulesOn;
std::string m_ModulesAuto;
CTimePoint m_LastNeedReloadTime;
time_t m_LastModTime;
};

View file

@ -87,10 +87,10 @@ int main(int argc, const char * argv[])
#endif
// check arguments
if ( argc != 4 )
if ( argc != 3 )
{
std::cout << "Usage: xlxd callsign xlxdip ambedip" << std::endl;
std::cout << "example: xlxd XLX999 192.168.178.212 127.0.0.1" << std::endl;
std::cout << "Usage: xlxd callsign xlxdip" << std::endl;
std::cout << "example: xlxd XLX999 192.168.178.212" << std::endl;
return 1;
}
@ -100,7 +100,7 @@ int main(int argc, const char * argv[])
// initialize reflector
g_Reflector.SetCallsign(argv[1]);
g_Reflector.SetListenIp(CIp(argv[2]));
g_Reflector.SetTranscoderIp(CIp(CIp(argv[3])));
g_Reflector.SetTranscoderIp(CIp("127.0.0.1")); // default if xlxd.transcoder does not exist
// and let it run
if ( !g_Reflector.Start() )

View file

@ -181,6 +181,7 @@
#define BLACKLIST_PATH "/xlxd/xlxd.blacklist"
#define INTERLINKLIST_PATH "/xlxd/xlxd.interlink"
#define TERMINALOPTIONS_PATH "/xlxd/xlxd.terminal"
#define TRANSCODEROPTIONS_PATH "/xlxd/xlxd.transcoder"
#define DEBUGDUMP_PATH "/var/log/xlxd.debug"
// system constants ---------------------------------------------

View file

@ -31,3 +31,6 @@ install:
[ -f /xlxd/xlxd.terminal ] && \
cp ../config/xlxd.terminal /xlxd/xlxd.terminal.sample || \
cp ../config/xlxd.terminal /xlxd/xlxd.terminal
[ -f /xlxd/xlxd.transcoder ] && \
cp ../config/xlxd.transcoder /xlxd/xlxd.transcoder.sample || \
cp ../config/xlxd.transcoder /xlxd/xlxd.transcoder