Escape semicolons in node names

fixes #312
This commit is contained in:
Johannes Meyer 2023-08-03 14:40:46 +02:00
parent be1f56ba08
commit 3fe17336dc
20 changed files with 223 additions and 62 deletions

View file

@ -3,6 +3,7 @@
namespace Icinga\Module\Businessprocess\Forms;
use Exception;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\Common\Sort;
@ -498,10 +499,13 @@ class AddNodeForm extends BpConfigBaseForm
case 'new-process':
$properties = $this->getValues();
unset($properties['name']);
if (! $properties['alias']) {
unset($properties['alias']);
}
if ($this->hasParentNode()) {
$properties['parentName'] = $this->parent->getName();
}
$changes->createNode($this->getValue('name'), $properties);
$changes->createNode(BpConfig::escapeName($this->getValue('name')), $properties);
break;
}

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
@ -29,9 +30,9 @@ class EditNodeForm extends BpConfigBaseForm
public function setup()
{
$this->host = substr($this->getNode()->getName(), 0, strpos($this->getNode()->getName(), ';'));
if ($this->isService()) {
$this->service = substr($this->getNode()->getName(), strpos($this->getNode()->getName(), ';') + 1);
[$this->host, $suffix] = BpConfig::splitNodeName($this->getNode()->getName());
if ($suffix !== 'Hoststatus') {
$this->service = $suffix;
}
$view = $this->getView();

View file

@ -471,7 +471,7 @@ class BpConfig
)
);
$node->setBpConfig($this);
$this->nodes[$host . ';' . $service] = $node;
$this->nodes[$node->getName()] = $node;
$this->hosts[$host] = true;
return $node;
}
@ -480,7 +480,7 @@ class BpConfig
{
$node = new HostNode((object) array('hostname' => $host));
$node->setBpConfig($this);
$this->nodes[$host . ';Hoststatus'] = $node;
$this->nodes[$node->getName()] = $node;
$this->hosts[$host] = true;
return $node;
}
@ -642,15 +642,13 @@ class BpConfig
// Fallback: if it is a service, create an empty one:
$this->warn(sprintf('The node "%s" doesn\'t exist', $name));
$pos = strpos($name, ';');
if ($pos !== false) {
$host = substr($name, 0, $pos);
$service = substr($name, $pos + 1);
// TODO: deactivated, this scares me, test it
if ($service === 'Hoststatus') {
return $this->createHost($host);
[$name, $suffix] = self::splitNodeName($name);
if ($suffix !== null) {
if ($suffix === 'Hoststatus') {
return $this->createHost($name);
} else {
return $this->createService($host, $service);
return $this->createService($name, $suffix);
}
}
@ -1015,4 +1013,61 @@ class BpConfig
return $data;
}
/**
* Escape the given node name
*
* @param string $name
*
* @return string
*/
public static function escapeName(string $name): string
{
return preg_replace('/((?<!\\\\);)/', '\\\\$1', $name);
}
/**
* Unescape the given node name
*
* @param string $name
*
* @return string
*/
public static function unescapeName(string $name): string
{
return str_replace('\\;', ';', $name);
}
/**
* Join the given two name parts together
*
* The used separator is the semicolon. If a semicolon exists in either part, it's escaped.
*
* @param string $name
* @param ?string $suffix
*
* @return string
*/
public static function joinNodeName(string $name, ?string $suffix = null): string
{
return self::escapeName($name) . ($suffix ? ";$suffix" : '');
}
/**
* Split the given node name into two parts
*
* The first part is always a string, with any semicolons unescaped.
* The second part may be null or a string otherwise.
*
* @param string $nodeName
*
* @return array
*/
public static function splitNodeName(string $nodeName): array
{
$parts = preg_split('/(?<!\\\\);/', $nodeName, 2);
$parts[0] = self::unescapeName($parts[0]);
return array_pad($parts, 2, null);
}
}

View file

@ -56,7 +56,8 @@ class BpNode extends Node
public function __construct($object)
{
$this->name = $object->name;
$this->name = BpConfig::escapeName($object->name);
$this->alias = BpConfig::unescapeName($object->name);
$this->operator = $object->operator;
$this->childNames = $object->child_names;
}

View file

@ -4,6 +4,7 @@ namespace Icinga\Module\Businessprocess\Common;
use Icinga\Application\Modules\Module;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\IcingaDbObject;
use Icinga\Module\Businessprocess\MonitoringRestrictions;
use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
@ -51,9 +52,8 @@ trait EnumList
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
$suffix = ';Hoststatus';
foreach ($names as $name) {
$res[$name . $suffix] = $name;
$res[BpConfig::joinNodeName($name, 'Hoststatus')] = $name;
}
return $res;
@ -76,7 +76,7 @@ trait EnumList
$services = array();
foreach ($names as $name) {
$services[$host . ';' . $name] = $name;
$services[BpConfig::joinNodeName($host, $name)] = $name;
}
return $services;
@ -100,9 +100,8 @@ trait EnumList
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
$suffix = ';Hoststatus';
foreach ($names as $name) {
$res[$name . $suffix] = $name;
$res[BpConfig::joinNodeName($name, 'Hoststatus')] = $name;
}
return $res;
@ -115,7 +114,8 @@ trait EnumList
if ($this->useIcingaDbBackend()) {
$objects = (new IcingaDbObject())->fetchServices($filter);
foreach ($objects as $object) {
$services[$object->host->name . ';' . $object->name] = $object->host->name . ':' . $object->name;
$services[BpConfig::joinNodeName($object->host->name, $object->name)]
= $object->host->name . ':' . $object->name;
}
} else {
$objects = $this->backend
@ -127,7 +127,8 @@ trait EnumList
->getQuery()
->fetchAll();
foreach ($objects as $object) {
$services[$object->host . ';' . $object->service] = $object->host . ':' . $object->service;
$services[BpConfig::joinNodeName($object->host, $object->service)]
= $object->host . ':' . $object->service;
}
}

View file

@ -35,7 +35,7 @@ class HostNode extends MonitoredNode
public function __construct($object)
{
$this->name = $object->hostname . ';Hoststatus';
$this->name = BpConfig::joinNodeName($object->hostname, 'Hoststatus');
$this->hostname = $object->hostname;
if (isset($object->state)) {
$this->setState($object->state);

View file

@ -28,7 +28,7 @@ class ImportedNode extends BpNode
{
$this->parentBp = $parentBp;
$this->configName = $object->configName;
$this->nodeName = $object->node;
$this->nodeName = BpConfig::escapeName($object->node);
parent::__construct((object) [
'name' => '@' . $this->configName . ':' . $this->nodeName,
@ -69,11 +69,7 @@ class ImportedNode extends BpNode
public function getAlias()
{
if ($this->alias === null) {
$this->alias = $this->importedNode()->getAlias();
}
return $this->alias;
return $this->importedNode()->getAlias();
}
public function getOperator()

View file

@ -33,13 +33,12 @@ class NodeAddChildrenAction extends NodeAction
foreach ($this->children as $name) {
if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) {
if (strpos($name, ';') !== false) {
list($host, $service) = preg_split('/;/', $name, 2);
if ($service === 'Hoststatus') {
$config->createHost($host);
[$prefix, $suffix] = BpConfig::splitNodeName($name);
if ($suffix !== null) {
if ($suffix === 'Hoststatus') {
$config->createHost($prefix);
} else {
$config->createService($host, $service);
$config->createService($prefix, $suffix);
}
} elseif ($name[0] === '@' && strpos($name, ':') !== false) {
list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Icingadb\Hook\HostActionsHook;
use Icinga\Module\Icingadb\Model\Host;
use ipl\Web\Widget\Link;
@ -15,7 +16,7 @@ class HostActions extends HostActionsHook
new Link(
$label,
'businessprocess/node/impact?name='
. rawurlencode($host->name . ';Hoststatus')
. rawurlencode(BpConfig::joinNodeName($host->name, 'Hoststatus'))
)
);
}

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Icingadb\Hook\ServiceActionsHook;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Web\Widget\Link;
@ -16,9 +17,7 @@ class ServiceActions extends ServiceActionsHook
$label,
sprintf(
'businessprocess/node/impact?name=%s',
rawurlencode(
sprintf('%s;%s', $service->host->name, $service->name)
)
rawurlencode(BpConfig::joinNodeName($service->host->name, $service->name))
)
)
);

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Monitoring\Hook\HostActionsHook;
use Icinga\Module\Monitoring\Object\Host;
@ -12,7 +13,7 @@ class HostActions extends HostActionsHook
$label = mt('businessprocess', 'Business Impact');
return array(
$label => 'businessprocess/node/impact?name='
. rawurlencode($host->getName() . ';Hoststatus')
. rawurlencode(BpConfig::joinNodeName($host->getName(), 'Hoststatus'))
);
}
}

View file

@ -4,6 +4,7 @@ namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
use Exception;
use Icinga\Application\Config;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
use Icinga\Module\Monitoring\Object\Service;
use Icinga\Web\Url;
@ -16,9 +17,7 @@ class ServiceActions extends ServiceActionsHook
return array(
$label => sprintf(
'businessprocess/node/impact?name=%s',
rawurlencode(
sprintf('%s;%s', $service->getHost()->getName(), $service->getName())
)
rawurlencode(BpConfig::joinNodeName($service->getHost()->getName(), $service->getName()))
)
);
}

View file

@ -19,7 +19,7 @@ class ServiceNode extends MonitoredNode
public function __construct($object)
{
$this->name = $object->hostname . ';' . $object->service;
$this->name = BpConfig::joinNodeName($object->hostname, $object->service);
$this->hostname = $object->hostname;
$this->service = $object->service;
if (isset($object->state)) {

View file

@ -99,9 +99,9 @@ class IcingaDbState
protected function handleDbRow($row, BpConfig $config, $objectName)
{
if ($objectName === 'service') {
$key = $row->host->name . ';' . $row->name;
$key = BpConfig::joinNodeName($row->host->name, $row->name);
} else {
$key = $row->name . ';Hoststatus';
$key = BpConfig::joinNodeName($row->name, 'Hoststatus');
}
// We fetch more states than we need, so skip unknown ones

View file

@ -115,12 +115,12 @@ class MonitoringState
protected function handleDbRow($row, BpConfig $config)
{
$key = $row->hostname;
if (property_exists($row, 'service')) {
$key .= ';' . $row->service;
} else {
$key .= ';Hoststatus';
}
$key = BpConfig::joinNodeName(
$row->hostname,
property_exists($row, 'service')
? $row->service
: 'Hoststatus'
);
// We fetch more states than we need, so skip unknown ones
if (! $config->hasNode($key)) {

View file

@ -230,7 +230,7 @@ class LegacyConfigParser
*/
protected function parseDisplay(&$line, BpConfig $bp)
{
list($display, $name, $desc) = preg_split('~\s*;\s*~', substr($line, 8), 3);
list($display, $name, $desc) = preg_split('~\s*(?<!\\\\);\s*~', substr($line, 8), 3);
$bp->getBpNode($name)->setAlias($desc)->setDisplay($display);
if ($display > 0) {
$bp->addRootNode($name);
@ -239,7 +239,7 @@ class LegacyConfigParser
protected function parseInfoUrl(&$line, BpConfig $bp)
{
list($name, $url) = preg_split('~\s*;\s*~', substr($line, 9), 2);
list($name, $url) = preg_split('~\s*(?<!\\\\);\s*~', substr($line, 9), 2);
$bp->getBpNode($name)->setInfoUrl($url);
}
@ -324,10 +324,6 @@ class LegacyConfigParser
list($name, $value) = preg_split('~\s*=\s*~', $line, 2);
if (strpos($name, ';') !== false) {
$this->parseError('No semicolon allowed in varname');
}
$op = '&';
if (preg_match_all('~(?<!\\\\)([\|\+&\!\%\^])~', $value, $m)) {
$op = implode('', $m[1]);
@ -359,15 +355,15 @@ class LegacyConfigParser
$cmps = preg_split('~\s*(?<!\\\\)\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY);
foreach ($cmps as $val) {
$val = preg_replace('~(\\\\([\|\+&\!\%\^]))~', '$2', $val);
if (strpos($val, ';') !== false) {
if (preg_match('~(?<!\\\\);~', $val)) {
if ($bp->hasNode($val)) {
$node->addChild($bp->getNode($val));
} else {
list($host, $service) = preg_split('~;~', $val, 2);
list($host, $service) = preg_split('~(?<!\\\\);~', $val, 2);
if ($service === 'Hoststatus') {
$node->addChild($bp->createHost($host));
$node->addChild($bp->createHost(str_replace('\\;', ';', $host)));
} else {
$node->addChild($bp->createService($host, $service));
$node->addChild($bp->createService(str_replace('\\;', ';', $host), $service));
}
}
} elseif ($val[0] === '@') {

View file

@ -0,0 +1,8 @@
############################################
#
# Title: Also With Semicolons
#
############################################
b\;ar =
display 1;b\;ar;Bar

View file

@ -0,0 +1,14 @@
############################################
#
# Title: With Semicolons
#
############################################
hostsAnd = host\;1;Hoststatus & host2;Hoststatus
servicesOr = host\;1;pi;ng | host2;ping | host3;ping
singleHost = host\;1;Hoststatus & to\;p & @also-with-semicolons:b\;ar
to\;p = hostsAnd & servicesOr & singleHost
display 1;to\;p;Top Node
info_url to\;p;https://top.example.com/
no\;alias =
display 1;no\;alias;no;alias

View file

@ -0,0 +1,49 @@
<?php
namespace Tests\Icinga\Module\Businessprocess;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Test\BaseTestCase;
class BpConfigTest extends BaseTestCase
{
public function testJoinNodeName()
{
$this->assertSame(
'foo;bar',
BpConfig::joinNodeName('foo', 'bar')
);
$this->assertSame(
'foo\;bar',
BpConfig::joinNodeName('foo;bar')
);
$this->assertSame(
'foo\;bar;baroof',
BpConfig::joinNodeName('foo;bar', 'baroof')
);
$this->assertSame(
'foo\;bar;bar;oof',
BpConfig::joinNodeName('foo;bar', 'bar;oof')
);
}
public function testSplitNodeName()
{
$this->assertSame(
['foo', 'bar'],
BpConfig::splitNodeName('foo;bar')
);
$this->assertSame(
['foo;bar', null],
BpConfig::splitNodeName('foo\;bar')
);
$this->assertSame(
['foo;bar', 'baroof'],
BpConfig::splitNodeName('foo\;bar;baroof')
);
$this->assertSame(
['foo;bar', 'bar;oof'],
BpConfig::splitNodeName('foo\;bar;bar;oof')
);
}
}

View file

@ -2,6 +2,8 @@
namespace Tests\Icinga\Module\Businessprocess\Storage;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\Test\BaseTestCase;
use Icinga\Module\Businessprocess\Storage\LegacyStorage;
@ -31,10 +33,12 @@ class LegacyStorageTest extends BaseTestCase
$keys = array_keys($this->makeInstance()->listProcesses());
$this->assertEquals(
array(
'also-with-semicolons',
'broken_wrong-operator',
'combined',
'simple_with-header',
'simple_without-header',
'with-semicolons'
),
$keys
);
@ -45,10 +49,12 @@ class LegacyStorageTest extends BaseTestCase
$keys = array_values($this->makeInstance()->listProcesses());
$this->assertEquals(
array(
'Also With Semicolons (also-with-semicolons)',
'broken_wrong-operator',
'combined',
'Simple with header (simple_with-header)',
'simple_without-header',
'With Semicolons (with-semicolons)'
),
$keys
);
@ -135,4 +141,35 @@ class LegacyStorageTest extends BaseTestCase
$this->makeInstance()->loadProcess('combined')
);
}
public function testConfigsWithNodesThatHaveSemicolonsInTheirNameCanBeParsed()
{
$bp = $this->makeInstance()->loadProcess('with-semicolons');
$this->assertInstanceOf($this->processClass, $bp);
$this->assertTrue($bp->hasNode('to\\;p'));
$this->assertSame(
'https://top.example.com/',
$bp->getNode('to\\;p')->getInfoUrl()
);
$this->assertTrue($bp->hasNode('host\;1;Hoststatus'));
$this->assertSame('host;1', $bp->getNode('host\;1;Hoststatus')->getHostname());
$this->assertTrue($bp->hasNode('host\;1;pi;ng'));
$this->assertSame('host;1', $bp->getNode('host\;1;pi;ng')->getHostname());
$this->assertSame('pi;ng', $bp->getNode('host\;1;pi;ng')->getServiceDescription());
$this->assertTrue($bp->hasNode('singleHost'));
$this->assertTrue($bp->getNode('singleHost')->hasChild('to\\;p'));
$this->assertInstanceOf(BpNode::class, $bp->getNode('to\\;p'));
$this->assertInstanceOf(BpNode::class, $bp->getNode('no\\;alias'));
$this->assertSame('no;alias', $bp->getNode('no\\;alias')->getAlias());
$this->assertTrue($bp->hasNode('@also-with-semicolons:b\;ar'));
$this->assertTrue($bp->getNode('singleHost')->hasChild('@also-with-semicolons:b\;ar'));
$this->assertInstanceOf(ImportedNode::class, $bp->getNode('@also-with-semicolons:b\;ar'));
}
}