Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions ci/apiv2/test_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from hashtopolis import Config
from hashtopolis import HashtopolisError
from utils import BaseTest


class ConfigTest(BaseTest):
model_class = Config

Expand All @@ -25,7 +27,7 @@ def test_patch_many(self):

newConfigs = Config.objects.filter(configId__lte='9')
for new_config, new_attribute in zip(newConfigs, attributes_to_change):
self.assertEqual(new_config.value, new_attribute)
self.assertEqual(new_config.value, new_attribute)

def test_expandables(self):
model_obj = Config.objects.get(pk=1)
Expand All @@ -45,4 +47,47 @@ def test_blacklist_chars(self):
config.save()

obj = Config.objects.get(item='blacklistChars')
self.assertEqual(obj.value, tmp_value)
self.assertEqual(obj.value, tmp_value)

def test_bounds_are_exposed_via_aggregate(self):
default_config = Config.objects.get(item='hashcatBrainPort')
self.assertFalse(hasattr(default_config, 'valueBoundaries'))

numeric_config = Config.objects.params(**{"aggregate[config]": "valueBoundaries"}).get(item='hashcatBrainPort')
self.assertTrue(hasattr(numeric_config, 'valueBoundaries'))
self.assertEqual(numeric_config.valueBoundaries['min'], 1)
self.assertEqual(numeric_config.valueBoundaries['max'], 65535)

field_separator = Config.objects.params(**{"aggregate[config]": "valueBoundaries"}).get(item='fieldseparator')
self.assertEqual(field_separator.valueBoundaries['maxLength'], 1)

tickbox_config = Config.objects.params(
**{"aggregate[config]": "valueBoundaries"}
).get(item='multicastTransferRateEnable')
self.assertEqual(tickbox_config.valueBoundaries['binaryValues'], ['0', '1'])

def test_numeric_bounds_are_validated(self):
config = Config.objects.get(item='hashcatBrainPort')
original_value = config.value

try:
config.value = '70000'
with self.assertRaises(HashtopolisError) as e:
config.save()
self.assertIn('at most 65535', e.exception.title)
finally:
config.value = original_value
config.save()

def test_field_separator_max_length_is_validated(self):
config = Config.objects.get(item='fieldseparator')
original_value = config.value

try:
config.value = '::'
with self.assertRaises(HashtopolisError) as e:
config.save()
self.assertIn('at most 1', e.exception.title)
finally:
config.value = original_value
config.save()
13 changes: 13 additions & 0 deletions ci/apiv2/test_global_config_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ def test_consistent_across_calls(self):
result1 = Helper().get_global_config()
result2 = Helper().get_global_config()
self.assertEqual(result1, result2)

def test_bounds_are_exposed(self):
configs = Helper().get_global_config()

port_config = next(c for c in configs if c.item == 'hashcatBrainPort')
self.assertEqual(port_config.min, 1)
self.assertEqual(port_config.max, 65535)

field_separator = next(c for c in configs if c.item == 'fieldseparator')
self.assertEqual(field_separator.maxLength, 1)

tickbox_config = next(c for c in configs if c.item == 'multicastTransferRateEnable')
self.assertEqual(tickbox_config.binaryValues, ['0', '1'])
10 changes: 10 additions & 0 deletions src/inc/apiv2/helper/GetGlobalConfigHelperAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Hashtopolis\inc\apiv2\error\HttpError;
use Hashtopolis\inc\apiv2\error\HttpForbidden;
use Hashtopolis\inc\HTException;
use Hashtopolis\inc\utils\ConfigUtils;
use JsonException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
Expand Down Expand Up @@ -88,5 +89,14 @@ static public function register($app): void {
public static function getResponse(): string {
return "Config";
}

protected function filterData(array $object): array {
$item = $object['item'] ?? null;
if (!is_string($item) || $item === '') {
return $object;
}

return array_merge($object, ConfigUtils::getConfigValueBounds($item));
}
}

16 changes: 16 additions & 0 deletions src/inc/apiv2/model/ConfigAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,20 @@ protected function updateObject(int $objectId, array $data): void {
protected function updateObjects(array $objects): void {
ConfigUtils::updateConfigs($objects);
}

public function getAggregateFieldsets(): array {
return [
'config' => [
'valueBoundaries' => [$this, 'getAggregateValueBoundaries'],
]
];
}

protected function getAggregateValueBoundaries(AbstractModel $object): ?array {
if (!($object instanceof Config) || !is_string($object->getItem()) || $object->getItem() === '') {
return null;
}

return ConfigUtils::getConfigValueBounds($object->getItem());
}
}
129 changes: 117 additions & 12 deletions src/inc/utils/ConfigUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Hashtopolis\dba\models\ConfigSection;
use Hashtopolis\dba\Factory;
use Hashtopolis\inc\defines\DConfig;
use Hashtopolis\inc\defines\DConfigType;
use Hashtopolis\inc\defines\DDirectories;
use Hashtopolis\inc\defines\DHashlistFormat;
use Hashtopolis\inc\defines\DLogEntry;
Expand All @@ -26,6 +27,112 @@
use Hashtopolis\inc\Util;

class ConfigUtils {
const DEFAULT_NUMBER_MIN = 0;
const DEFAULT_NUMBER_MAX = 999999999;
const DEFAULT_STRING_MAX_LENGTH = 65535;

/**
* @return array<string, mixed>
*/
public static function getConfigValueBounds(string $item): array {
$type = DConfig::getConfigType($item);
$bounds = [];

if ($type === DConfigType::NUMBER_INPUT) {
$bounds = [
'min' => self::DEFAULT_NUMBER_MIN,
'max' => self::DEFAULT_NUMBER_MAX,
];
}
else if ($type === DConfigType::TICKBOX) {
$bounds = [
'binaryValues' => ['0', '1'],
];
}
else if ($type === DConfigType::STRING_INPUT || $type === DConfigType::EMAIL || $type === DConfigType::SELECT) {
$bounds = [
'maxLength' => self::DEFAULT_STRING_MAX_LENGTH,
];
}

$itemBounds = match ($item) {
DConfig::HASHCAT_BRAIN_PORT,
DConfig::NOTIFICATIONS_PROXY_PORT => ['min' => 1, 'max' => 65535],
DConfig::DISP_TOLERANCE,
DConfig::AGENT_UTIL_THRESHOLD_1,
DConfig::AGENT_UTIL_THRESHOLD_2 => ['min' => 0, 'max' => 100],
DConfig::HASHES_PAGE_SIZE,
DConfig::HASHES_PER_PAGE,
DConfig::DEFAULT_PAGE_SIZE,
DConfig::MAX_PAGE_SIZE => ['min' => 1],
DConfig::EMAIL_SENDER,
DConfig::CONTACT_EMAIL => ['maxLength' => 320],
DConfig::FIELD_SEPARATOR => ['maxLength' => 1],
default => [],
};

return array_merge($bounds, $itemBounds);
}

/**
* @throws HTException
*/
public static function validateAndNormalizeConfigValue(string $item, mixed $value): string {
if (is_null($value)) {
throw new HTException("No new config value provided");
}

$type = DConfig::getConfigType($item);
$bounds = self::getConfigValueBounds($item);

if ($type === DConfigType::TICKBOX) {
if (in_array($value, [true, 1, '1', 'true'], true)) {
return '1';
}
if (in_array($value, [false, 0, '0', 'false', ''], true)) {
return '0';
}
throw new HTException("Value most be boolean!");
}

if ($type === DConfigType::NUMBER_INPUT) {
if (!is_numeric($value)) {
throw new HTException("Value must be numeric!");
}

$numericValue = $value + 0;
if (isset($bounds['min']) && $numericValue < $bounds['min']) {
throw new HTException("Value must be at least " . $bounds['min'] . "!");
}
if (isset($bounds['max']) && $numericValue > $bounds['max']) {
throw new HTException("Value must be at most " . $bounds['max'] . "!");
}

return (string)$value;
}

if (!is_scalar($value)) {
throw new HTException("Value must be scalar!");
}
$normalizedValue = (string)$value;

if (isset($bounds['maxLength']) && strlen($normalizedValue) > $bounds['maxLength']) {
throw new HTException("Value length must be at most " . $bounds['maxLength'] . "!");
}

if ($type === DConfigType::EMAIL && !filter_var($normalizedValue, FILTER_VALIDATE_EMAIL)) {
throw new HTException("Value must be email!");
}

if ($type === DConfigType::SELECT) {
if (!in_array($normalizedValue, DConfig::getSelection($item)->getKeys(), true)) {
throw new HTException("Value is not in selection!");
}
}

return $normalizedValue;
}

/**
* @param Config $config
* @param boolean $new
Expand Down Expand Up @@ -92,13 +199,10 @@ public static function updateSingleConfig($id, $attributes): void {
if (is_null($currentConfig)) {
throw new HTException("No config with this ID!");
}
$newValue = $attributes[Config::VALUE] ?? null;
$name = $currentConfig->getItem();

if (is_null($newValue)) {
throw new HTException("No new config value provided");
}
if ($currentConfig->getValue() === $newValue) {

$newValue = self::validateAndNormalizeConfigValue($name, $attributes[Config::VALUE] ?? null);
if ((string)$currentConfig->getValue() === $newValue) {
return; //The value was not changed so we don't need to update it.
}

Expand Down Expand Up @@ -143,31 +247,32 @@ public static function updateConfig(array $arr): void {
foreach ($arr as $item => $val) {
if (str_starts_with($item, "config_")) {
$name = substr($item, 7);
if (SConfig::getInstance()->getVal($name) == $val) {
$newValue = self::validateAndNormalizeConfigValue($name, $val);
if ((string)SConfig::getInstance()->getVal($name) === $newValue) {
continue; // the value was not changed, so we don't need to update it
}

$qF = new QueryFilter(Config::ITEM, $name, "=");
$config = Factory::getConfigFactory()->filter([Factory::FILTER => $qF], true);
if ($config == null) {
$config = new Config(null, self::DEFAULT_CONFIG_SECTION, $name, $val);
$config = new Config(null, self::DEFAULT_CONFIG_SECTION, $name, $newValue);
Factory::getConfigFactory()->save($config);
}
else {
if ($name == DConfig::HASH_MAX_LENGTH) {
$limit = intval($val);
$limit = intval($newValue);
if (!Util::setMaxHashLength($limit)) {
throw new HTException("Failed to update max hash length!");
}
}
else if ($name == DConfig::PLAINTEXT_MAX_LENGTH) {
$limit = intval($val);
$limit = intval($newValue);
if (!Util::setPlaintextMaxLength($limit)) {
throw new HTException("Failed to update max plaintext length!");
}
}
SConfig::getInstance()->addValue($name, $val);
$config->setValue($val);
SConfig::getInstance()->addValue($name, $newValue);
$config->setValue($newValue);
ConfigUtils::set($config, false);
}
}
Expand Down
Loading