Skip to content

Commit

Permalink
Tighten config parser
Browse files Browse the repository at this point in the history
  • Loading branch information
quinox committed Mar 1, 2024
1 parent db0d7a0 commit f7831a1
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 23 deletions.
73 changes: 73 additions & 0 deletions FlashMQTests/tst_maintests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ See LICENSE for license details.
*/

#include "tst_maintests.h"
#include "configfileparser.h"
#include "exceptions.h"
#include "settings.h"

#include <list>
#include <unordered_map>
Expand Down Expand Up @@ -505,6 +508,76 @@ void MainTests::test_loading_acl_file()
QVERIFY(true);
}

void MainTests::test_parsing_numbers()
{
/*
const Settings *settings = ThreadGlobals::getSettings();
ConfFileTemp humanConfig;
humanConfig.writeLine("expire_sessions_after_seconds 180pqn\n");
humanConfig.closeFile();
std::vector<std::string> args {"--config-file", humanConfig.getFilePath()};
cleanup();
init(args);
QCOMPARE(settings->expireSessionsAfterSeconds, (u_int32_t) 180);
*/

/* this should work */
ConfFileTemp *humanConfig = new ConfFileTemp();
humanConfig->writeLine("expire_sessions_after_seconds 180\n");
humanConfig->closeFile();

ConfigFileParser *parser = new ConfigFileParser(humanConfig->getFilePath());
parser->loadFile(false);

Settings settings = parser->getSettings();

QCOMPARE(settings.expireSessionsAfterSeconds, (u_int32_t) 180);

/* this should fail: 180days */
humanConfig = new ConfFileTemp();
humanConfig->writeLine("expire_sessions_after_seconds 180days\n");
humanConfig->closeFile();

parser = new ConfigFileParser(humanConfig->getFilePath());
try {
parser->loadFile(false);
QFAIL("The parser was too liberal");
} catch (ConfigFileException) {
/* good */
}

/* this should also fail: 180 days */
humanConfig = new ConfFileTemp();
humanConfig->writeLine("expire_sessions_after_seconds 180 days\n");
humanConfig->closeFile();

parser = new ConfigFileParser(humanConfig->getFilePath());
try {
parser->loadFile(false);
QFAIL("The parser was too liberal");
} catch (ConfigFileException) {
/* good */
}

/* Last one that should fail: 180 days and a bit */
humanConfig = new ConfFileTemp();
humanConfig->writeLine("expire_sessions_after_seconds 180 days and a bit more\n");
humanConfig->closeFile();

parser = new ConfigFileParser(humanConfig->getFilePath());
try {
parser->loadFile(false);
QFAIL("The parser was too liberal");
} catch (ConfigFileException) {
/* good */
}

}

#ifndef FMQ_NO_SSE
void MainTests::test_sse_split()
{
Expand Down
1 change: 1 addition & 0 deletions FlashMQTests/tst_maintests.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ private slots:
void test_acl_patterns_username();
void test_acl_patterns_clientid();
void test_loading_acl_file();
void test_parsing_numbers();

void test_validUtf8Generic();
#ifndef FMQ_NO_SSE
Expand Down
76 changes: 53 additions & 23 deletions configfileparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ See LICENSE for license details.
#include "utils.h"
#include "globber.h"

/* Like std::stoi, but demands that the entire value is consumed */
int full_stoi(const std::string &key, const std::string value) {
size_t ptr;
int newVal = std::stoi(value, &ptr);
if (ptr != value.length()) {
throw ConfigFileException(formatString("%s's value of '%s' can't be parsed to a number", key.c_str(), value.c_str()));
}
return newVal;
}

/* Like std::stoul, but demands that the entire value is consumed */
unsigned long full_stoul(const std::string &key, const std::string value) {
size_t ptr;
int newVal = std::stoul(value, &ptr);
if (ptr != value.length()) {
throw ConfigFileException(formatString("%s's value of '%s' can't be parsed to a number", key.c_str(), value.c_str()));
}
return newVal;
}

/**
* @brief ConfigFileParser::testKeyValidity tests if two strings match and whether it's a valid config key.
Expand Down Expand Up @@ -366,6 +385,7 @@ void ConfigFileParser::loadFile(bool test)

std::string key = matches[1].str();
const std::string value = matches[2].str();
int number_of_expected_values = 1; // Most lines only accept 1 argument, a select few 2.
std::string valueTrimmed = value;
trim(valueTrimmed);

Expand All @@ -381,7 +401,7 @@ void ConfigFileParser::loadFile(bool test)
}
else if (testKeyValidity(key, "port", validListenKeys))
{
curListener->port = std::stoi(value);
curListener->port = full_stoi("port", value);
}
else if (testKeyValidity(key, "fullchain", validListenKeys))
{
Expand Down Expand Up @@ -460,7 +480,7 @@ void ConfigFileParser::loadFile(bool test)
}
if (testKeyValidity(key, "remote_session_expiry_interval", validBridgeKeys))
{
int64_t newVal = std::stoi(value);
int64_t newVal = full_stoi("remote_session_expiry_interval", value);
if (newVal <= 0 || newVal > std::numeric_limits<uint32_t>::max())
{
throw ConfigFileException(formatString("Value '%d' doesn't fit in uint32_t.", newVal));
Expand All @@ -473,7 +493,7 @@ void ConfigFileParser::loadFile(bool test)
}
if (testKeyValidity(key, "local_session_expiry_interval", validBridgeKeys))
{
int64_t newVal = std::stoi(value);
int64_t newVal = full_stoi("local_session_expiry_interval", value);
if (newVal <= 0 || newVal > std::numeric_limits<uint32_t>::max())
{
throw ConfigFileException(formatString("Value '%d' doesn't fit in uint32_t.", newVal));
Expand All @@ -489,11 +509,12 @@ void ConfigFileParser::loadFile(bool test)

if (matches.size() == 4)
{
number_of_expected_values = 2;
const std::string &qosstr = matches[3];

if (!qosstr.empty())
{
topicPath.qos = std::stoul(qosstr);
topicPath.qos = full_stoul("subscribe qos", qosstr);

if (!topicPath.isValidQos())
throw ConfigFileException(formatString("Qos '%s' is not a valid qos level", qosstr.c_str()));
Expand All @@ -512,11 +533,12 @@ void ConfigFileParser::loadFile(bool test)

if (matches.size() == 4)
{
number_of_expected_values = 2;
const std::string &qosstr = matches[3];

if (!qosstr.empty())
{
topicPath.qos = std::stoul(qosstr);
topicPath.qos = full_stoul("publish qos", qosstr);

if (!topicPath.isValidQos())
throw ConfigFileException(formatString("Qos '%s' is not a valid qos level", qosstr.c_str()));
Expand All @@ -542,7 +564,7 @@ void ConfigFileParser::loadFile(bool test)
}
if (testKeyValidity(key, "port", validBridgeKeys))
{
const int64_t newVal = std::stoi(value);
const int64_t newVal = full_stoi("port", value);
if (newVal <= 0 || newVal > std::numeric_limits<uint16_t>::max())
{
throw ConfigFileException(formatString("Value '%d' doesn't fit in uint16_t.", newVal));
Expand Down Expand Up @@ -570,7 +592,7 @@ void ConfigFileParser::loadFile(bool test)
}
if (testKeyValidity(key, "keepalive", validBridgeKeys))
{
int64_t newVal = std::stoi(value);
int64_t newVal = full_stoi("keepalive", value);
if (newVal < 10 || newVal > std::numeric_limits<uint16_t>::max())
{
throw ConfigFileException(formatString("Value '%d' doesn't fit in uint16_t and must be at least 10.", newVal));
Expand Down Expand Up @@ -604,7 +626,7 @@ void ConfigFileParser::loadFile(bool test)
}
if (testKeyValidity(key, "max_incoming_topic_aliases", validBridgeKeys))
{
const int64_t newVal = std::stoi(value);
const int64_t newVal = full_stoi("max_incoming_topic_aliases", value);
if (newVal < 0 || newVal > std::numeric_limits<uint16_t>::max())
{
throw ConfigFileException(formatString("Value '%d' doesn't fit in uint16_t.", newVal));
Expand All @@ -613,7 +635,7 @@ void ConfigFileParser::loadFile(bool test)
}
if (testKeyValidity(key, "max_outgoing_topic_aliases", validBridgeKeys))
{
const int64_t newVal = std::stoi(value);
const int64_t newVal = full_stoi("max_outgoing_topic_aliases", value);
if (newVal < 0 || newVal > std::numeric_limits<uint16_t>::max())
{
throw ConfigFileException(formatString("Value '%d' doesn't fit in uint16_t.", newVal));
Expand Down Expand Up @@ -696,15 +718,15 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "client_initial_buffer_size", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("client_initial_buffer_size", value);
if (!isPowerOfTwo(newVal))
throw ConfigFileException("client_initial_buffer_size value " + value + " is not a power of two.");
tmpSettings.clientInitialBufferSize = newVal;
}

if (testKeyValidity(key, "max_packet_size", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("max_packet_size", value);
if (newVal > ABSOLUTE_MAX_PACKET_SIZE)
{
std::ostringstream oss;
Expand Down Expand Up @@ -746,7 +768,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "rlimit_nofile", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("rlimit_nofile", value);
if (newVal <= 0)
{
throw ConfigFileException(formatString("Value '%d' is negative.", newVal));
Expand All @@ -756,7 +778,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "expire_sessions_after_seconds", validKeys))
{
uint32_t newVal = std::stoi(value);
uint32_t newVal = full_stoi("expire_sessions_after_seconds", value);
if (newVal > 0 && newVal < 60) // 0 means disable
{
throw ConfigFileException(formatString("expire_sessions_after_seconds value '%d' is invalid. Valid values are 0, or 60 or higher.", newVal));
Expand All @@ -766,7 +788,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "plugin_timer_period", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("plugin_timer_period", value);
if (newVal < 0)
{
throw ConfigFileException(formatString("plugin_timer_period value '%d' is invalid. Valid values are 0 or higher. 0 means disabled.", newVal));
Expand All @@ -784,7 +806,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "thread_count", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("thread_count", value);
if (newVal < 0)
{
throw ConfigFileException(formatString("thread_count value '%d' is invalid. Valid values are 0 or higher. 0 means auto.", newVal));
Expand All @@ -794,7 +816,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "max_qos_msg_pending_per_client", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("max_qos_msg_pending_per_client", value);
if (newVal < 32 || newVal > 65535)
{
throw ConfigFileException(formatString("max_qos_msg_pending_per_client value '%d' is invalid. Valid values between 32 and 65535.", newVal));
Expand All @@ -804,7 +826,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "max_qos_bytes_pending_per_client", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("max_qos_bytes_pending_per_client", value);
if (newVal < 4096)
{
throw ConfigFileException(formatString("max_qos_bytes_pending_per_client value '%d' is invalid. Valid values are 4096 or higher.", newVal));
Expand All @@ -814,7 +836,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "max_incoming_topic_alias_value", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("max_incoming_topic_alias_value", value);
if (newVal < 0 || newVal > 0xFFFF)
{
throw ConfigFileException(formatString("max_incoming_topic_alias_value value '%d' is invalid. Valid values are between 0 and 65535.", newVal));
Expand All @@ -824,7 +846,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "max_outgoing_topic_alias_value", validKeys))
{
int newVal = std::stoi(value);
int newVal = full_stoi("max_outgoing_topic_alias_value", value);
if (newVal < 0 || newVal > 0xFFFF)
{
throw ConfigFileException(formatString("max_outgoing_topic_alias_value value '%d' is invalid. Valid values are between 0 and 65535.", newVal));
Expand Down Expand Up @@ -856,7 +878,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "expire_retained_messages_after_seconds", validKeys))
{
uint32_t newVal = std::stoi(value);
uint32_t newVal = full_stoi("expire_retained_messages_after_seconds", value);
if (newVal < 1)
{
throw ConfigFileException(formatString("expire_retained_messages_after_seconds value '%d' is invalid. Valid values are between 1 and 4294967296.", newVal));
Expand Down Expand Up @@ -890,7 +912,7 @@ void ConfigFileParser::loadFile(bool test)
if (testKeyValidity(key, "client_max_write_buffer_size", validKeys))
{
const uint32_t minVal = 4096;
uint32_t newVal = std::stoul(value);
uint32_t newVal = full_stoul("client_max_write_buffer_size", value);

if (newVal < minVal)
throw ConfigFileException(formatString("Value '%s' for '%s' is too low. It must be at least %d.", value.c_str(), key.c_str(), minVal));
Expand All @@ -905,7 +927,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "retained_messages_node_limit", validKeys))
{
const uint32_t newVal = std::stoul(value);
const uint32_t newVal = full_stoul("retained_messages_node_limit", value);

if (newVal == 0)
throw ConfigFileException("Set '" + key + "' higher than 0, or use 'retained_messages_mode'.");
Expand All @@ -915,7 +937,7 @@ void ConfigFileParser::loadFile(bool test)

if (testKeyValidity(key, "minimum_wildcard_subscription_depth", validKeys))
{
const unsigned long newVal = std::stoul(value);
const unsigned long newVal = full_stoul("minimum_wildcard_subscription_depth", value);

if (newVal > 0xFFFF)
throw ConfigFileException("Option '" + key + "' must be between 0 and 65535.");
Expand All @@ -940,6 +962,14 @@ void ConfigFileParser::loadFile(bool test)
{
throw ConfigFileException(ex.what());
}

int encountered_values = matches.size() - 2; // 0 = full line, 1 = key, 2 = first value, 3 = second value etc.
if (encountered_values != number_of_expected_values) {
const std::string &rest = matches[number_of_expected_values + 2];
if (!rest.empty()) {
throw ConfigFileException(formatString("Option %s expected %d, got %d arguments (%s ...) while parsing '%s'", key.c_str(), number_of_expected_values, encountered_values, rest.c_str(), line.c_str()));
}
}
}

tmpSettings.checkUniqueBridgeNames();
Expand Down

0 comments on commit f7831a1

Please sign in to comment.