Merge branch '2.2' into develop

This commit is contained in:
Markus Mäkelä
2017-11-27 09:11:25 +02:00
157 changed files with 6042 additions and 1673 deletions

View File

@ -1352,9 +1352,9 @@ static bool cache_rule_matches_column_simple(CACHE_RULE *self, const char *defau
{
matches = !matches;
}
}
++i;
++i;
}
if (tables)
{

View File

@ -173,8 +173,9 @@ const struct store_test_case store_test_cases[] =
STORE_TEST_CASE("query", "!=", "SELECT a FROM tbl", false, NULL, "SELECT a FROM tbl"),
STORE_TEST_CASE("query", "=", "SELECT b FROM tbl", false, NULL, "SELECT a FROM tbl"),
STORE_TEST_CASE("query", "!=", "SELECT b FROM tbl", true, NULL, "SELECT a FROM tbl"),
STORE_TEST_CASE("column", "=", "a", false, NULL, "SELECT b FROM tbl WHERE a = 5"),
// We are no longer able to distinguish selected columns
// from one used in the WHERE-clause.
STORE_TEST_CASE("column", "=", "a", true, NULL, "SELECT b FROM tbl WHERE a = 5"),
STORE_TEST_CASE("column", "=", "a", true, NULL, "SELECT a, b FROM tbl WHERE a = 5"),
};
@ -242,7 +243,9 @@ int main()
pConfig->n_threads = 1;
set_libdir(MXS_STRDUP_A("../../../../../query_classifier/qc_sqlite/"));
if (qc_setup("qc_sqlite", QC_SQL_MODE_DEFAULT, "") && qc_process_init(QC_INIT_BOTH))
if (qc_setup("qc_sqlite", QC_SQL_MODE_DEFAULT, "") &&
qc_process_init(QC_INIT_BOTH) &&
qc_thread_init(QC_INIT_BOTH))
{
set_libdir(MXS_STRDUP_A("../"));
rc = test();

View File

@ -21,6 +21,10 @@ if(BISON_FOUND AND FLEX_FOUND)
target_link_libraries(dbfwchk maxscale-common MySQLCommon dbfwfilter-core)
install_executable(dbfwchk core)
if(BUILD_TESTS)
add_subdirectory(test)
endif()
else()
message(FATAL_ERROR "Could not find Bison or Flex: ${BISON_EXECUTABLE} ${FLEX_EXECUTABLE}")
endif()

View File

@ -67,6 +67,7 @@
#include <stdlib.h>
#include <algorithm>
#include <sstream>
#include <map>
#include <maxscale/atomic.h>
#include <maxscale/modulecmd.h>
@ -97,16 +98,27 @@ namespace
{
/** The rules and users for each thread */
struct DbfwThread
class DbfwThread
{
DbfwThread():
rule_version(0)
{
}
public:
int& rule_version(const Dbfw* d) { return m_instance_data[d].rule_version; }
RuleList& rules(const Dbfw* d) { return m_instance_data[d].rules; }
UserMap& users(const Dbfw* d) { return m_instance_data[d].users; }
int rule_version;
RuleList rules;
UserMap users;
private:
class Data
{
public:
Data()
: rule_version(0)
{}
int rule_version;
RuleList rules;
UserMap users;
};
std::map<const Dbfw*, Data> m_instance_data;
};
thread_local DbfwThread* this_thread = NULL;
@ -140,7 +152,7 @@ static json_t* rules_to_json(const RuleList& rules)
{
json_t* rval = json_array();
for (RuleList::const_iterator it = this_thread->rules.begin(); it != this_thread->rules.end(); it++)
for (RuleList::const_iterator it = rules.begin(); it != rules.end(); it++)
{
const SRule& rule = *it;
json_array_append_new(rval, rule_to_json(rule));
@ -410,7 +422,10 @@ bool dbfw_show_rules(const MODULECMD_ARG *argv, json_t** output)
dcb_printf(dcb, "Rule, Type, Times Matched\n");
if (this_thread->rules.empty() || this_thread->users.empty())
RuleList& rules = this_thread->rules(inst);
UserMap& users = this_thread->users(inst);
if (rules.empty() || users.empty())
{
if (!replace_rules(inst))
{
@ -418,7 +433,7 @@ bool dbfw_show_rules(const MODULECMD_ARG *argv, json_t** output)
}
}
for (RuleList::const_iterator it = this_thread->rules.begin(); it != this_thread->rules.end(); it++)
for (RuleList::const_iterator it = rules.begin(); it != rules.end(); it++)
{
const SRule& rule = *it;
char buf[rule->name().length() + 200]; // Some extra space
@ -436,7 +451,10 @@ bool dbfw_show_rules_json(const MODULECMD_ARG *argv, json_t** output)
json_t* arr = json_array();
if (this_thread->rules.empty() || this_thread->users.empty())
RuleList& rules = this_thread->rules(inst);
UserMap& users = this_thread->users(inst);
if (rules.empty() || users.empty())
{
if (!replace_rules(inst))
{
@ -444,7 +462,7 @@ bool dbfw_show_rules_json(const MODULECMD_ARG *argv, json_t** output)
}
}
for (RuleList::const_iterator it = this_thread->rules.begin(); it != this_thread->rules.end(); it++)
for (RuleList::const_iterator it = rules.begin(); it != rules.end(); it++)
{
const SRule& rule = *it;
json_array_append_new(arr, rule_to_json(rule));
@ -870,11 +888,11 @@ void define_columns_rule(void* scanner)
*
* @param scanner Current scanner
*/
void define_function_rule(void* scanner)
void define_function_rule(void* scanner, bool inverted)
{
struct parser_stack* rstack = (struct parser_stack*)dbfw_yyget_extra((yyscan_t) scanner);
ss_dassert(rstack);
rstack->add(new FunctionRule(rstack->name, rstack->values));
rstack->add(new FunctionRule(rstack->name, rstack->values, inverted));
}
/**
@ -894,11 +912,11 @@ void define_function_usage_rule(void* scanner)
*
* @param scanner Current scanner
*/
void define_column_function_rule(void* scanner)
void define_column_function_rule(void* scanner, bool inverted)
{
struct parser_stack* rstack = (struct parser_stack*)dbfw_yyget_extra((yyscan_t) scanner);
ss_dassert(rstack);
rstack->add(new ColumnFunctionRule(rstack->name, rstack->values, rstack->auxiliary_values));
rstack->add(new ColumnFunctionRule(rstack->name, rstack->values, rstack->auxiliary_values, inverted));
}
/**
@ -1092,11 +1110,11 @@ bool replace_rules(Dbfw* instance)
if (process_rule_file(filename, &rules, &users))
{
this_thread->rules.swap(rules);
this_thread->users.swap(users);
this_thread->rules(instance).swap(rules);
this_thread->users(instance).swap(users);
rval = true;
}
else if (!this_thread->rules.empty() && !this_thread->users.empty())
else if (!this_thread->rules(instance).empty() && !this_thread->users(instance).empty())
{
MXS_ERROR("Failed to parse rules at '%s'. Old rules are still used.",
filename.c_str());
@ -1116,25 +1134,40 @@ static bool update_rules(Dbfw* my_instance)
bool rval = true;
int rule_version = my_instance->get_rule_version();
if (this_thread->rule_version < rule_version)
if (this_thread->rule_version(my_instance) < rule_version)
{
if (!replace_rules(my_instance))
{
rval = false;
}
this_thread->rule_version = rule_version;
this_thread->rule_version(my_instance) = rule_version;
}
return rval;
}
namespace
{
/**
* Global rule version. Every time a Dbfw instance is created, its rule version will
* be the value of this variable, which is then incremented by one.
*
* This is to ensure that each created Dbfw instance will have a unique set of rules,
* irrespective of whether a new instance is created in exactly the same memory location
* where an earlier, now deleted instance existed.
*/
int global_version = 1;
};
Dbfw::Dbfw(MXS_CONFIG_PARAMETER* params):
m_action((enum fw_actions)config_get_enum(params, "action", action_values)),
m_log_match(0),
m_lock(SPINLOCK_INIT),
m_filename(config_get_string(params, "rules")),
m_version(1)
m_version(atomic_add(&global_version, 1))
{
if (config_get_bool(params, "log_match"))
{
@ -1254,11 +1287,16 @@ static SUser find_user_data(const UserMap& users, std::string name, std::string
if (it == users.end())
{
snprintf(nameaddr, sizeof(nameaddr), "%%@%s", remote.c_str());
ip_start = strchr(nameaddr, '@') + 1;
it = users.find(nameaddr);
while (it == users.end() && next_ip_class(ip_start))
if (it == users.end())
{
it = users.find(nameaddr);
ip_start = strchr(nameaddr, '@') + 1;
while (it == users.end() && next_ip_class(ip_start))
{
it = users.find(nameaddr);
}
}
}
}
@ -1409,7 +1447,7 @@ int DbfwSession::routeQuery(GWBUF* buffer)
ss_dassert(analyzed_queue);
}
SUser suser = find_user_data(this_thread->users, user(), remote());
SUser suser = find_user_data(this_thread->users(m_instance), user(), remote());
bool query_ok = false;
if (command_is_mandatory(buffer))
@ -1673,7 +1711,9 @@ void Dbfw::diagnostics(DCB *dcb) const
dcb_printf(dcb, "Firewall Filter\n");
dcb_printf(dcb, "Rule, Type, Times Matched\n");
for (RuleList::const_iterator it = this_thread->rules.begin(); it != this_thread->rules.end(); it++)
RuleList& rules = this_thread->rules(this);
for (RuleList::const_iterator it = rules.begin(); it != rules.end(); it++)
{
const SRule& rule = *it;
char buf[rule->name().length() + 200];
@ -1694,5 +1734,5 @@ void Dbfw::diagnostics(DCB *dcb) const
*/
json_t* Dbfw::diagnostics_json() const
{
return rules_to_json(this_thread->rules);
return rules_to_json(this_thread->rules(this));
}

View File

@ -43,9 +43,9 @@ void define_wildcard_rule(void* scanner);
void define_where_clause_rule(void* scanner);
bool define_regex_rule(void* scanner, char* pattern);
void define_columns_rule(void* scanner);
void define_function_rule(void* scanner);
void define_function_rule(void* scanner, bool inverted);
void define_function_usage_rule(void* scanner);
void define_column_function_rule(void* scanner);
void define_column_function_rule(void* scanner, bool inverted);
void define_limit_queries_rule(void* scanner, int max, int timeperiod, int holdoff);
bool add_at_times_rule(void* scanner, const char* range);
void add_on_queries_rule(void* scanner, const char* sql);

View File

@ -37,7 +37,7 @@
%token FWTOK_RULE FWTOK_USERS FWTOK_RULES FWTOK_ANY FWTOK_ALL
%token FWTOK_STRICT_ALL FWTOK_MATCH FWTOK_WILDCARD FWTOK_COLUMNS FWTOK_REGEX
%token FWTOK_LIMIT_QUERIES FWTOK_WHERE_CLAUSE FWTOK_AT_TIMES FWTOK_ON_QUERIES
%token FWTOK_FUNCTION FWTOK_USES_FUNCTION FWTOK_COMMENT FWTOK_PIPE
%token FWTOK_FUNCTION FWTOK_USES_FUNCTION FWTOK_COMMENT FWTOK_PIPE FWTOK_NOT_FUNCTION
/** Terminal typed symbols */
%token <floatval>FWTOK_FLOAT <strval>FWTOK_TIME <strval>FWTOK_BTSTR
@ -124,9 +124,15 @@ mandatory
{define_limit_queries_rule(scanner, $2, $3, $4);}
| FWTOK_REGEX FWTOK_QUOTEDSTR {define_regex_rule(scanner, $2);}
| FWTOK_COLUMNS valuelist {define_columns_rule(scanner);}
| FWTOK_FUNCTION valuelist {define_function_rule(scanner);}
| FWTOK_FUNCTION {define_function_rule(scanner);}
| FWTOK_FUNCTION valuelist FWTOK_COLUMNS auxiliaryvaluelist {define_column_function_rule(scanner);}
| FWTOK_FUNCTION valuelist {define_function_rule(scanner, false);}
| FWTOK_NOT_FUNCTION valuelist {define_function_rule(scanner, true);}
| FWTOK_NOT_FUNCTION {define_function_rule(scanner, true);}
| FWTOK_FUNCTION valuelist FWTOK_COLUMNS auxiliaryvaluelist
{define_column_function_rule(scanner, false);}
| FWTOK_NOT_FUNCTION valuelist FWTOK_COLUMNS auxiliaryvaluelist
{define_column_function_rule(scanner, true);}
| FWTOK_NOT_FUNCTION FWTOK_COLUMNS auxiliaryvaluelist
{define_column_function_rule(scanner, true);}
| FWTOK_USES_FUNCTION valuelist {define_function_usage_rule(scanner);}
;

View File

@ -41,8 +41,11 @@ Rule::~Rule()
bool Rule::matches_query(DbfwSession* session, GWBUF* buffer, char** msg) const
{
*msg = create_error("Permission denied at this time.");
MXS_NOTICE("rule '%s': query denied at this time.", name().c_str());
MXS_NOTICE("rule '%s': query matches at this time.", name().c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Permission denied at this time.");
}
return true;
}
@ -92,8 +95,11 @@ bool WildCardRule::matches_query(DbfwSession* session, GWBUF* buffer, char** msg
if (strcmp(infos[i].column, "*") == 0)
{
MXS_NOTICE("rule '%s': query contains a wildcard.", name().c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Usage of wildcard denied.");
}
rval = true;
*msg = create_error("Usage of wildcard denied.");
}
}
}
@ -107,10 +113,12 @@ bool NoWhereClauseRule::matches_query(DbfwSession* session, GWBUF* buffer, char*
if (query_is_sql(buffer) && !qc_query_has_clause(buffer))
{
MXS_NOTICE("rule '%s': query has no where/having clause.", name().c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Required WHERE/HAVING clause is missing.");
}
rval = true;
*msg = create_error("Required WHERE/HAVING clause is missing.");
MXS_NOTICE("rule '%s': query has no where/having "
"clause, query is denied.", name().c_str());
}
return rval;
@ -133,8 +141,11 @@ bool RegexRule::matches_query(DbfwSession* session, GWBUF* buffer, char** msg) c
if (pcre2_match(re, (PCRE2_SPTR)sql, (size_t)len, 0, 0, mdata, NULL) > 0)
{
MXS_NOTICE("rule '%s': regex matched on query", name().c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Permission denied, query matched regular expression.");
}
rval = true;
*msg = create_error("Permission denied, query matched regular expression.");
}
pcre2_match_data_free(mdata);
@ -161,9 +172,12 @@ bool ColumnsRule::matches_query(DbfwSession* session, GWBUF* buffer, char** msg)
if (it != m_values.end())
{
MXS_NOTICE("rule '%s': query targets forbidden column: %s",
MXS_NOTICE("rule '%s': query targets specified column: %s",
name().c_str(), tok.c_str());
*msg = create_error("Permission denied to column '%s'.", tok.c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Permission denied to column '%s'.", tok.c_str());
}
rval = true;
break;
}
@ -184,27 +198,23 @@ bool FunctionRule::matches_query(DbfwSession* session, GWBUF* buffer, char** msg
size_t n_infos;
qc_get_function_info(buffer, &infos, &n_infos);
if (n_infos == 0 && session->get_action() == FW_ACTION_ALLOW)
for (size_t i = 0; i < n_infos; ++i)
{
rval = true;
}
else
{
for (size_t i = 0; i < n_infos; ++i)
std::string tok = infos[i].name;
std::transform(tok.begin(), tok.end(), tok.begin(), ::tolower);
ValueList::const_iterator it = std::find(m_values.begin(), m_values.end(), tok);
if ((!m_inverted && (it != m_values.end())) ||
(m_inverted && (it == m_values.end())))
{
std::string tok = infos[i].name;
std::transform(tok.begin(), tok.end(), tok.begin(), ::tolower);
ValueList::const_iterator it = std::find(m_values.begin(), m_values.end(), tok);
if (it != m_values.end())
MXS_NOTICE("rule '%s': query matches function: %s",
name().c_str(), tok.c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
MXS_NOTICE("rule '%s': query uses forbidden function: %s",
name().c_str(), tok.c_str());
*msg = create_error("Permission denied to function '%s'.", tok.c_str());
rval = true;
break;
}
rval = true;
break;
}
}
}
@ -230,9 +240,12 @@ bool FunctionUsageRule::matches_query(DbfwSession* session, GWBUF* buffer, char*
if (it != m_values.end())
{
MXS_NOTICE("rule '%s': query uses a function with forbidden column: %s",
MXS_NOTICE("rule '%s': query uses a function with specified column: %s",
name().c_str(), tok.c_str());
*msg = create_error("Permission denied to column '%s' with function.", tok.c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Permission denied to column '%s' with function.", tok.c_str());
}
return true;
}
}
@ -259,7 +272,8 @@ bool ColumnFunctionRule::matches_query(DbfwSession* session, GWBUF* buffer, char
m_values.end(),
func);
if (func_it != m_values.end())
if ((!m_inverted && (func_it != m_values.end())) ||
(m_inverted && (func_it == m_values.end())))
{
/** The function matches, now check if the column matches */
@ -273,14 +287,16 @@ bool ColumnFunctionRule::matches_query(DbfwSession* session, GWBUF* buffer, char
if (col_it != m_columns.end())
{
MXS_NOTICE("rule '%s': query uses function '%s' with forbidden column: %s",
MXS_NOTICE("rule '%s': query uses function '%s' with specified column: %s",
name().c_str(), col.c_str(), func.c_str());
*msg = create_error("Permission denied to column '%s' with function '%s'.",
col.c_str(), func.c_str());
if (session->get_action() == FW_ACTION_BLOCK)
{
*msg = create_error("Permission denied to column '%s' with function '%s'.",
col.c_str(), func.c_str());
}
return true;
}
}
}
}
}

View File

@ -180,12 +180,16 @@ class FunctionRule: public ValueListRule
FunctionRule& operator=(const FunctionRule&);
public:
FunctionRule(std::string name, const ValueList& values):
ValueListRule(name, "FUNCTION", values)
FunctionRule(std::string name, const ValueList& values, bool inverted):
ValueListRule(name, inverted ? "NOT_FUNCTION" : "FUNCTION", values),
m_inverted(inverted)
{
}
bool matches_query(DbfwSession* session, GWBUF* buffer, char** msg) const;
private:
bool m_inverted; /*< Should the match be inverted. */
};
/**
@ -214,16 +218,18 @@ class ColumnFunctionRule: public ValueListRule
ColumnFunctionRule& operator=(const ColumnFunctionRule&);
public:
ColumnFunctionRule(std::string name, const ValueList& values, const ValueList& columns):
ValueListRule(name, "COLUMN_FUNCTION", values),
m_columns(columns)
ColumnFunctionRule(std::string name, const ValueList& values, const ValueList& columns, bool inverted):
ValueListRule(name, inverted ? "NOT_COLUMN_FUNCTION" : "COLUMN_FUNCTION", values),
m_columns(columns),
m_inverted(inverted)
{
}
bool matches_query(DbfwSession* session, GWBUF* buffer, char** msg) const;
private:
ValueList m_columns; /*< List of columns to match */
ValueList m_columns; /*< List of columns to match */
bool m_inverted; /*< Should the match be inverted. */
};
/**

View File

@ -0,0 +1,19 @@
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../test)
add_executable(test_dbfwfilter
test_dbfwfilter.cc
tempfile.cc
../../test/filtermodule.cc
../../test/mock.cc
../../test/mock_backend.cc
../../test/mock_client.cc
../../test/mock_dcb.cc
../../test/mock_routersession.cc
../../test/mock_session.cc
../../test/module.cc
../../test/queryclassifiermodule.cc
)
target_link_libraries(test_dbfwfilter maxscale-common)
add_test(test_dbfwfilter test_dbfwfilter)

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "tempfile.hh"
#include <string.h>
#include <unistd.h>
#include <maxscale/debug.h>
namespace
{
static char NAME_TEMPLATE[] = "/tmp/XXXXXX";
}
TempFile::TempFile()
: m_fd(-1)
, m_name(NAME_TEMPLATE)
{
m_fd = mkstemp((char*)m_name.c_str());
ss_dassert(m_fd != -1);
}
TempFile::~TempFile()
{
int rc = unlink(m_name.c_str());
ss_dassert(rc != -1);
close(m_fd);
}
void TempFile::write(const void* pData, size_t count)
{
int rc = ::write(m_fd, pData, count);
ss_dassert(rc != -1);
ss_dassert((size_t)rc == count);
}
void TempFile::write(const char* zData)
{
write(zData, strlen(zData));
}

View File

@ -0,0 +1,69 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <string>
/**
* TempFile is a class using which a temporary file can be created and
* written to.
*/
class TempFile
{
TempFile(const TempFile&);
TempFile& operator = (const TempFile&);
public:
/**
* Constructor
*
* Create temporary file in /tmp
*/
TempFile();
/**
* Destructor
*
* Close and unlink file.
*/
~TempFile();
/**
* The name of the created temporary file.
*
* @return The name of the file.
*/
std::string name() const
{
return m_name;
}
/**
* Write data to the file.
*
* @param pData Pointer to data.
* @param count The number of bytes to write.
*/
void write(const void* pData, size_t count);
/**
* Write data to the file.
*
* @param zData Null terminated buffer to write.
*/
void write(const char* zData);
private:
int m_fd;
std::string m_name;
};

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,7 @@ CMP [=<>!]+
deny|allow MXS_WARNING("Use of '%s' is deprecated, use 'match' instead", yytext);return FWTOK_MATCH;
rule return FWTOK_RULE;
function return FWTOK_FUNCTION;
not_function return FWTOK_NOT_FUNCTION;
uses_function return FWTOK_USES_FUNCTION;
no_where_clause return FWTOK_WHERE_CLAUSE;
wildcard return FWTOK_WILDCARD;

View File

@ -25,6 +25,7 @@
#include <maxscale/cppdefs.hh>
#include <cmath>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
@ -60,6 +61,7 @@ enum log_options
LOG_DATA_DATE = (1 << 2),
LOG_DATA_USER = (1 << 3),
LOG_DATA_QUERY = (1 << 4),
LOG_DATA_REPLY_TIME = (1 << 5),
};
/* Default values for logged data */
@ -71,7 +73,9 @@ static MXS_FILTER_SESSION *newSession(MXS_FILTER *instance, MXS_SESSION *session
static void closeSession(MXS_FILTER *instance, MXS_FILTER_SESSION *session);
static void freeSession(MXS_FILTER *instance, MXS_FILTER_SESSION *session);
static void setDownstream(MXS_FILTER *instance, MXS_FILTER_SESSION *fsession, MXS_DOWNSTREAM *downstream);
static void setUpstream(MXS_FILTER *instance, MXS_FILTER_SESSION *session, MXS_UPSTREAM *upstream);
static int routeQuery(MXS_FILTER *instance, MXS_FILTER_SESSION *fsession, GWBUF *queue);
static int clientReply(MXS_FILTER *instance, MXS_FILTER_SESSION *session, GWBUF *queue);
static void diagnostic(MXS_FILTER *instance, MXS_FILTER_SESSION *fsession, DCB *dcb);
static json_t* diagnostic_json(const MXS_FILTER *instance, const MXS_FILTER_SESSION *fsession);
static uint64_t getCapabilities(MXS_FILTER* instance);
@ -108,10 +112,39 @@ typedef struct
bool write_warning_given;
} QLA_INSTANCE;
/**
* Helper struct for holding data before it's written to file.
*/
struct LOG_EVENT_DATA
{
bool has_message; // Does message data exist?
GWBUF* query_clone; // Clone of the query buffer.
char query_date[QLA_DATE_BUFFER_SIZE]; // Text representation of date.
timespec begin_time; // Timer value at the moment of receiving query.
};
namespace
{
/**
* Resets event data. Since QLA_SESSION is allocated with calloc, separate initialisation is not needed.
*
* @param event Event to reset
*/
void clear(LOG_EVENT_DATA& event)
{
event.has_message = false;
gwbuf_free(event.query_clone);
event.query_clone = NULL;
event.query_date[0] = '\0';
event.begin_time = {0, 0};
}
}
/* The session structure for this QLA filter. */
typedef struct
{
int active;
MXS_UPSTREAM up;
MXS_DOWNSTREAM down;
char *filename; /* The session-specific log file name */
FILE *fp; /* The session-specific log file */
@ -120,11 +153,12 @@ typedef struct
size_t ses_id; /* The session this filter serves */
const char *user; /* The client */
pcre2_match_data* match_data; /* Regex match data */
LOG_EVENT_DATA event_data; /* Information about the latest event, required if logging execution time. */
} QLA_SESSION;
static FILE* open_log_file(uint32_t, QLA_INSTANCE *, const char *);
static int write_log_entry(uint32_t, FILE*, QLA_INSTANCE*, QLA_SESSION*, const char*,
const char*, size_t);
static int write_log_entry(uint32_t, FILE*, QLA_INSTANCE*, QLA_SESSION*,
const char*, const char*, size_t, int);
static bool cb_log(const MODULECMD_ARG *argv, json_t** output);
static const MXS_ENUM_VALUE option_values[] =
@ -144,11 +178,12 @@ static const MXS_ENUM_VALUE log_type_values[] =
static const MXS_ENUM_VALUE log_data_values[] =
{
{"service", LOG_DATA_SERVICE},
{"session", LOG_DATA_SESSION},
{"date", LOG_DATA_DATE},
{"user", LOG_DATA_USER},
{"query", LOG_DATA_QUERY},
{"service", LOG_DATA_SERVICE},
{"session", LOG_DATA_SESSION},
{"date", LOG_DATA_DATE},
{"user", LOG_DATA_USER},
{"query", LOG_DATA_QUERY},
{"reply_time", LOG_DATA_REPLY_TIME},
{NULL}
};
@ -199,9 +234,9 @@ MXS_MODULE* MXS_CREATE_MODULE()
closeSession,
freeSession,
setDownstream,
NULL, // No Upstream requirement
setUpstream,
routeQuery,
NULL, // No client reply
clientReply,
diagnostic,
diagnostic_json,
getCapabilities,
@ -483,6 +518,7 @@ closeSession(MXS_FILTER *instance, MXS_FILTER_SESSION *session)
{
fclose(my_session->fp);
}
clear(my_session->event_data);
}
/**
@ -518,6 +554,65 @@ setDownstream(MXS_FILTER *instance, MXS_FILTER_SESSION *session, MXS_DOWNSTREAM
my_session->down = *downstream;
}
/**
* Set the upstream filter or router to which queries will be
* passed from this filter.
*
* @param instance The filter instance data
* @param session The filter session
* @param upstream The upstream filter or router.
*/
static void
setUpstream(MXS_FILTER *instance, MXS_FILTER_SESSION *session, MXS_UPSTREAM *upstream)
{
QLA_SESSION *my_session = (QLA_SESSION *) session;
my_session->up = *upstream;
}
/**
* Write QLA log entry/entries to disk
*
* @param my_instance Filter instance
* @param my_session Filter session
* @param query Query string, not 0-terminated
* @param querylen Query string length
* @param date_string Date string
* @param elapsed_ms Query execution time, in milliseconds
*/
void write_log_entries(QLA_INSTANCE* my_instance, QLA_SESSION* my_session,
const char* query, int querylen, const char* date_string, int elapsed_ms)
{
bool write_error = false;
if (my_instance->log_mode_flags & CONFIG_FILE_SESSION)
{
// In this case there is no need to write the session
// number into the files.
uint32_t data_flags = (my_instance->log_file_data_flags &
~LOG_DATA_SESSION);
if (write_log_entry(data_flags, my_session->fp, my_instance, my_session,
date_string, query, querylen, elapsed_ms) < 0)
{
write_error = true;
}
}
if (my_instance->log_mode_flags & CONFIG_FILE_UNIFIED)
{
uint32_t data_flags = my_instance->log_file_data_flags;
if (write_log_entry(data_flags, my_instance->unified_fp, my_instance, my_session,
date_string, query, querylen, elapsed_ms) < 0)
{
write_error = true;
}
}
if (write_error && !my_instance->write_warning_given)
{
MXS_ERROR("qla-filter '%s': Log file write failed. "
"Suppressing further similar warnings.",
my_instance->name);
my_instance->write_warning_given = true;
}
}
/**
* The routeQuery entry point. This is passed the query buffer
* to which the filter should be applied. Once applied the
@ -533,56 +628,46 @@ routeQuery(MXS_FILTER *instance, MXS_FILTER_SESSION *session, GWBUF *queue)
{
QLA_INSTANCE *my_instance = (QLA_INSTANCE *) instance;
QLA_SESSION *my_session = (QLA_SESSION *) session;
char *ptr = NULL;
int length = 0;
struct tm t;
struct timeval tv;
char *query = NULL;
int query_len = 0;
if (my_session->active &&
modutil_extract_SQL(queue, &ptr, &length) &&
modutil_extract_SQL(queue, &query, &query_len) &&
mxs_pcre2_check_match_exclude(my_instance->re_match, my_instance->re_exclude,
my_session->match_data, ptr, length, MXS_MODULE_NAME))
my_session->match_data, query, query_len, MXS_MODULE_NAME))
{
char buffer[QLA_DATE_BUFFER_SIZE];
gettimeofday(&tv, NULL);
localtime_r(&tv.tv_sec, &t);
strftime(buffer, sizeof(buffer), "%F %T", &t);
/**
* Loop over all the possible log file modes and write to
* the enabled files.
*/
char *sql_string = ptr;
bool write_error = false;
if (my_instance->log_mode_flags & CONFIG_FILE_SESSION)
const uint32_t data_flags = my_instance->log_file_data_flags;
LOG_EVENT_DATA& event = my_session->event_data;
if (data_flags & LOG_DATA_DATE)
{
// In this case there is no need to write the session
// number into the files.
uint32_t data_flags = (my_instance->log_file_data_flags &
~LOG_DATA_SESSION);
if (write_log_entry(data_flags, my_session->fp,
my_instance, my_session, buffer, sql_string, length) < 0)
{
write_error = true;
}
// Print current date to a buffer. Use the buffer in the event data struct even if execution time
// is not needed.
const time_t utc_seconds = time(NULL);
tm local_time;
localtime_r(&utc_seconds, &local_time);
strftime(event.query_date, QLA_DATE_BUFFER_SIZE, "%F %T", &local_time);
}
if (my_instance->log_mode_flags & CONFIG_FILE_UNIFIED)
if (data_flags & LOG_DATA_REPLY_TIME)
{
uint32_t data_flags = my_instance->log_file_data_flags;
if (write_log_entry(data_flags, my_instance->unified_fp,
my_instance, my_session, buffer, sql_string, length) < 0)
// Have to measure reply time from server. Save query data for printing during clientReply.
// If old event data exists, it is erased. This only happens if client sends a query before
// receiving reply to previous query.
if (event.has_message)
{
write_error = true;
clear(event);
}
clock_gettime(CLOCK_MONOTONIC, &event.begin_time);
if (data_flags & LOG_DATA_QUERY)
{
event.query_clone = gwbuf_clone(queue);
}
event.has_message = true;
}
if (write_error && !my_instance->write_warning_given)
else
{
MXS_ERROR("qla-filter '%s': Log file write failed. "
"Suppressing further similar warnings.",
my_instance->name);
my_instance->write_warning_given = true;
// If execution times are not logged, write the log entry now.
write_log_entries(my_instance, my_session, query, query_len, event.query_date, -1);
}
}
/* Pass the query downstream */
@ -590,6 +675,41 @@ routeQuery(MXS_FILTER *instance, MXS_FILTER_SESSION *session, GWBUF *queue)
my_session->down.session, queue);
}
/**
* The clientReply entry point. Required for measuring and printing query execution time.
*
* @param instance The filter instance data
* @param session The filter session
* @param queue The query data
*/
static int
clientReply(MXS_FILTER *instance, MXS_FILTER_SESSION *session, GWBUF *queue)
{
QLA_INSTANCE *my_instance = (QLA_INSTANCE *) instance;
QLA_SESSION *my_session = (QLA_SESSION *) session;
LOG_EVENT_DATA& event = my_session->event_data;
if (event.has_message)
{
const uint32_t data_flags = my_instance->log_file_data_flags;
ss_dassert(data_flags & LOG_DATA_REPLY_TIME);
char* query = NULL;
int query_len = 0;
if (data_flags & LOG_DATA_QUERY)
{
modutil_extract_SQL(event.query_clone, &query, &query_len);
}
timespec now;
clock_gettime(CLOCK_MONOTONIC, &now); // Gives time in seconds + nanoseconds
// Calculate elapsed time in milliseconds.
double elapsed_ms = 1E3 * (now.tv_sec - event.begin_time.tv_sec) +
(now.tv_nsec - event.begin_time.tv_nsec) / (double)1E6;
write_log_entries(my_instance, my_session, query, query_len, event.query_date,
std::floor(elapsed_ms + 0.5));
clear(event);
}
return my_session->up.clientReply(my_session->up.instance, my_session->up.session, queue);
}
/**
* Diagnostics routine
*
@ -731,9 +851,9 @@ static FILE* open_log_file(uint32_t data_flags, QLA_INSTANCE *instance, const ch
const char DATE[] = "Date,";
const char USERHOST[] = "User@Host,";
const char QUERY[] = "Query,";
const char REPLY_TIME[] = "Reply_time,";
const int headerlen = sizeof(SERVICE) + sizeof(SERVICE) + sizeof(DATE) +
sizeof(USERHOST) + sizeof(QUERY);
sizeof(USERHOST) + sizeof(QUERY) + sizeof(REPLY_TIME);
char print_str[headerlen];
memset(print_str, '\0', headerlen);
@ -759,6 +879,11 @@ static FILE* open_log_file(uint32_t data_flags, QLA_INSTANCE *instance, const ch
strcat(current_pos, USERHOST);
current_pos += sizeof(USERHOST) - 1;
}
if (instance->log_file_data_flags & LOG_DATA_REPLY_TIME)
{
strcat(current_pos, REPLY_TIME);
current_pos += sizeof(REPLY_TIME) - 1;
}
if (instance->log_file_data_flags & LOG_DATA_QUERY)
{
strcat(current_pos, QUERY);
@ -800,11 +925,12 @@ static FILE* open_log_file(uint32_t data_flags, QLA_INSTANCE *instance, const ch
* @param time_string Date entry
* @param sql_string SQL-query, *not* NULL terminated
* @param sql_str_len Length of SQL-string
* @param elapsed_ms Query execution time, in milliseconds
* @return The number of characters written, or a negative value on failure
*/
static int write_log_entry(uint32_t data_flags, FILE *logfile, QLA_INSTANCE *instance,
QLA_SESSION *session, const char *time_string, const char *sql_string,
size_t sql_str_len)
size_t sql_str_len, int elapsed_ms)
{
ss_dassert(logfile != NULL);
size_t print_len = 0;
@ -816,13 +942,14 @@ static int write_log_entry(uint32_t data_flags, FILE *logfile, QLA_INSTANCE *ins
*/
// The numbers have some extra for delimiters.
const size_t integer_chars = 20; // Enough space for any integer type
if (data_flags & LOG_DATA_SERVICE)
{
print_len += strlen(session->service) + 1;
}
if (data_flags & LOG_DATA_SESSION)
{
print_len += 20; // To print a 64bit integer
print_len += integer_chars;
}
if (data_flags & LOG_DATA_DATE)
{
@ -832,6 +959,10 @@ static int write_log_entry(uint32_t data_flags, FILE *logfile, QLA_INSTANCE *ins
{
print_len += strlen(session->user) + strlen(session->remote) + 2;
}
if (data_flags & LOG_DATA_REPLY_TIME)
{
print_len += integer_chars;
}
if (data_flags & LOG_DATA_QUERY)
{
print_len += sql_str_len + 1; // Can't use strlen, not null-terminated
@ -898,6 +1029,17 @@ static int write_log_entry(uint32_t data_flags, FILE *logfile, QLA_INSTANCE *ins
current_pos += rval;
}
}
if (!error && (data_flags & LOG_DATA_REPLY_TIME))
{
if ((rval = sprintf(current_pos, "%d,", elapsed_ms)) < 0)
{
error = true;
}
else
{
current_pos += rval;
}
}
if (!error && (data_flags & LOG_DATA_QUERY))
{
strncat(current_pos, sql_string, sql_str_len); // non-null-terminated string

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include "maxscale/filtermodule.hh"
#include "../../../core/internal/modules.h"
using std::auto_ptr;
namespace maxscale
{
//
// FilterModule
//
const char* FilterModule::zName = MODULE_FILTER;
auto_ptr<FilterModule::Instance> FilterModule::createInstance(const char* zName,
char** pzOptions,
MXS_CONFIG_PARAMETER* pParameters)
{
auto_ptr<Instance> sInstance;
MXS_FILTER* pFilter = m_pApi->createInstance(zName, pzOptions, pParameters);
if (pFilter)
{
sInstance.reset(new Instance(this, pFilter));
}
return sInstance;
}
//
// FilterModule::Instance
//
FilterModule::Instance::Instance(FilterModule* pModule, MXS_FILTER* pInstance)
: m_module(*pModule)
, m_pInstance(pInstance)
{
}
FilterModule::Instance::~Instance()
{
m_module.destroyInstance(m_pInstance);
}
auto_ptr<FilterModule::Session> FilterModule::Instance::newSession(MXS_SESSION* pSession)
{
auto_ptr<Session> sFilter_session;
MXS_FILTER_SESSION* pFilter_session = m_module.newSession(m_pInstance, pSession);
if (pFilter_session)
{
sFilter_session.reset(new Session(this, pFilter_session));
}
return sFilter_session;
}
//
// FilterModule::Session
//
FilterModule::Session::Session(Instance* pInstance, MXS_FILTER_SESSION* pFilter_session)
: m_instance(*pInstance)
, m_pFilter_session(pFilter_session)
{
}
FilterModule::Session::~Session()
{
m_instance.freeSession(m_pFilter_session);
}
}

View File

@ -0,0 +1,198 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <memory>
#include <maxscale/filter.hh>
#include "module.hh"
namespace maxscale
{
/**
* An instance of FilterModule represents a filter module.
*/
class FilterModule : public SpecificModule<FilterModule>
{
FilterModule(const FilterModule&);
FilterModule& operator = (const FilterModule&);
public:
static const char* zName; /*< The name describing the module type. */
typedef MXS_FILTER_OBJECT type_t; /*< The type of the module object. */
class Session;
class Instance
{
Instance(const Instance&);
Instance& operator = (const Instance&);
public:
~Instance();
/**
* Create a new filter session.
*
* @param pSession The session to which the filter session belongs.
*
* @return A new filter session or NULL if the creation failed.
*/
std::auto_ptr<Session> newSession(MXS_SESSION* pSession);
private:
friend class FilterModule;
Instance(FilterModule* pModule, MXS_FILTER* pInstance);
private:
friend class Session;
void freeSession(MXS_FILTER_SESSION* pFilter_session)
{
m_module.freeSession(m_pInstance, pFilter_session);
}
int routeQuery(MXS_FILTER_SESSION* pFilter_session, GWBUF* pStatement)
{
return m_module.routeQuery(m_pInstance, pFilter_session, pStatement);
}
int clientReply(MXS_FILTER_SESSION* pFilter_session, GWBUF* pStatement)
{
return m_module.clientReply(m_pInstance, pFilter_session, pStatement);
}
void setDownstream(MXS_FILTER_SESSION* pFilter_session, MXS_DOWNSTREAM* pDownstream)
{
m_module.setDownstream(m_pInstance, pFilter_session, pDownstream);
}
void setUpstream(MXS_FILTER_SESSION* pFilter_session, MXS_UPSTREAM* pUpstream)
{
m_module.setUpstream(m_pInstance, pFilter_session, pUpstream);
}
private:
FilterModule& m_module;
MXS_FILTER* m_pInstance;
};
class Session
{
Session(const Session&);
Session& operator = (const Session&);
public:
~Session();
/**
* The following member functions correspond to the MaxScale filter API.
*/
int routeQuery(GWBUF* pStatement)
{
return m_instance.routeQuery(m_pFilter_session, pStatement);
}
int clientReply(GWBUF* pBuffer)
{
return m_instance.clientReply(m_pFilter_session, pBuffer);
}
void setDownstream(MXS_DOWNSTREAM* pDownstream)
{
m_instance.setDownstream(m_pFilter_session, pDownstream);
}
void setUpstream(MXS_UPSTREAM* pUpstream)
{
m_instance.setUpstream(m_pFilter_session, pUpstream);
}
private:
friend class Instance;
Session(Instance* pInstance, MXS_FILTER_SESSION* pFilter_session);
private:
Instance& m_instance;
MXS_FILTER_SESSION* m_pFilter_session;
};
/**
* Create a new instance.
*
* @param zName The name of the instance (config file section name),
* @param pzOptions Optional options.
* @param pParameters Configuration parameters.
*
* @return A new instance or NULL if creation failed.
*/
std::auto_ptr<Instance> createInstance(const char* zName,
char** pzOptions,
MXS_CONFIG_PARAMETER* pParameters);
private:
friend class Instance;
void destroyInstance(MXS_FILTER* pInstance)
{
m_pApi->destroyInstance(pInstance);
}
MXS_FILTER_SESSION* newSession(MXS_FILTER* pInstance, MXS_SESSION* pSession)
{
return m_pApi->newSession(pInstance, pSession);
}
void freeSession(MXS_FILTER* pInstance, MXS_FILTER_SESSION* pFilter_session)
{
m_pApi->freeSession(pInstance, pFilter_session);
}
int routeQuery(MXS_FILTER* pInstance, MXS_FILTER_SESSION* pFilter_session, GWBUF* pStatement)
{
return m_pApi->routeQuery(pInstance, pFilter_session, pStatement);
}
int clientReply(MXS_FILTER* pInstance, MXS_FILTER_SESSION* pFilter_session, GWBUF* pStatement)
{
return m_pApi->clientReply(pInstance, pFilter_session, pStatement);
}
void setDownstream(MXS_FILTER* pInstance,
MXS_FILTER_SESSION* pFilter_session,
MXS_DOWNSTREAM* pDownstream)
{
m_pApi->setDownstream(pInstance, pFilter_session, pDownstream);
}
void setUpstream(MXS_FILTER* pInstance,
MXS_FILTER_SESSION* pFilter_session,
MXS_UPSTREAM* pUpstream)
{
m_pApi->setUpstream(pInstance, pFilter_session, pUpstream);
}
private:
friend class SpecificModule<FilterModule>;
FilterModule(MXS_FILTER_OBJECT* pApi)
: m_pApi(pApi)
{
}
private:
MXS_FILTER_OBJECT* m_pApi;
};
}

View File

@ -0,0 +1,140 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <map>
#include "routersession.hh"
namespace maxscale
{
namespace mock
{
/**
* The abstract class Backend represents a backend.
*/
class Backend
{
Backend(const Backend&);
Backend& operator = (const Backend&);
public:
virtual ~Backend();
/**
* Called to handle a statement from a "client".
*
* @param pSession The originating router session.
* @param pStatement A buffer containing a statement.
*/
virtual void handle_statement(RouterSession* pSession, GWBUF* pStatement) = 0;
/**
* Called when the backend should respond to the client.
*
* @param pSession The router session to respond to.
*
* @return True, if the backend has additional responses to the router session.
*/
virtual bool respond(RouterSession* pSession) = 0;
/**
* Whether the backend has a response for some router.
*
* @param pSession A router session.
*
* @return True if there are responses for the router session.
*/
virtual bool idle(const RouterSession* pSession) const = 0;
/**
* Discards an available response.
*
* @param pSession A router session.
*
* @return True if there are additional responses for the router session.
*/
virtual bool discard_one_response(const RouterSession* pSession) = 0;
/**
* Discards all available responses.
*
* @param pSession A router session.
*/
virtual void discard_all_responses(const RouterSession* pSession) = 0;
protected:
Backend();
};
/**
* The abstract class BufferBackend is a helper class for concrete
* backend classes.
*/
class BufferBackend : public Backend
{
BufferBackend(const BufferBackend&);
BufferBackend& operator = (const BufferBackend&);
public:
~BufferBackend();
bool respond(RouterSession* pSession);
bool idle(const RouterSession* pSession) const;
bool discard_one_response(const RouterSession* pSession);
void discard_all_responses(const RouterSession* pSession);
protected:
BufferBackend();
/**
* Enqueues a response for a particular router session.
*
* @param pSession The session to enqueue the response for.
* @param pResponse The response.
*/
void enqueue_response(const RouterSession* pSession, GWBUF* pResponse);
private:
GWBUF* dequeue_response(const RouterSession* pSession, bool* pEmpty);
private:
typedef std::deque<GWBUF*> Responses;
typedef std::map<const RouterSession*, Responses> SessionResponses;
SessionResponses m_session_responses;
};
/**
* The OkBackend is a concrete backend class that response with an
* OK packet to all statements.
*/
class OkBackend : public BufferBackend
{
OkBackend(const OkBackend&);
OkBackend& operator = (const OkBackend&);
public:
OkBackend();
void handle_statement(RouterSession* pSession, GWBUF* pStatement);
};
}
}

View File

@ -0,0 +1,141 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <memory>
#include <string>
#include <maxscale/filter.h>
#include "../filtermodule.hh"
#include "dcb.hh"
namespace maxscale
{
namespace mock
{
/**
* An instance of Client represents a client. It can be used as the
* upstream filter of another filter.
*/
class Client : public MXS_FILTER_SESSION
, public Dcb::Handler
{
Client(const Client&);
Client& operator = (const Client&);
public:
/**
* A Handler can be used for processing responses.
*/
class Handler
{
public:
virtual ~Handler();
/**
* Called when a response is received from the backend.
*
* @param pResponse The response packet.
*
* @return 1 if processing should continue, 0 otherwise.
*/
virtual int32_t backend_reply(GWBUF* pResponse) = 0;
/**
* Called when a response is sent directly by a filter.
*
* @param pResponse The response packet.
*
* @return 1 if processing should continue, 0 otherwise.
*/
virtual int32_t maxscale_reply(GWBUF* pResponse) = 0;
/**
* Called when @reset is called on the @c Client instance.
*/
virtual void reset();
};
/**
* Constructor
*
* @param zUser The client of the session,
* @param zHost The host of the client.
* @param pHandler Optional response handler.
*/
Client(const char* zUser,
const char* zHost,
Handler* pHandler = NULL);
~Client();
/**
* @return The name of the client.
*/
const char* user() const;
/**
* @return The name of the host.
*/
const char* host() const;
/**
* Set a response handler
*
* @param pHandler The new response handler.
*
* @return The previous response handler.
*/
Handler* set_handler(Handler* pHandler);
/**
* How many responses have been handled.
*
* @return The number of responses since last call to @c reset.
*/
size_t n_responses() const;
/**
* Reset the Client object. The number of counted responsed will
* be set to 0. If the Client object has a handler, then its @c reset
* function will be called as well.
*/
void reset();
/**
* Set this object as client filter of provided filter.
*
* @param session The filter session whose upstream filter should be set.
*/
void set_as_upstream_on(FilterModule::Session& session);
private:
int32_t clientReply(GWBUF* pResponse);
static int32_t clientReply(MXS_FILTER* pInstance, MXS_FILTER_SESSION* pSession, GWBUF* pResponse);
// Dcb::Handler
int32_t write(GWBUF* pBuffer);
private:
MXS_FILTER m_instance;
std::string m_user;
std::string m_host;
Handler* m_pHandler;
size_t m_n_responses;
};
}
}

View File

@ -0,0 +1,83 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "mock.hh"
#include <maxscale/dcb.h>
#include <maxscale/session.h>
namespace maxscale
{
namespace mock
{
/**
* The class Dcb provides a mock DCB that can be used when testing.
*/
class Dcb : public DCB
{
Dcb(const Dcb&);
Dcb& operator = (const Dcb&);
public:
class Handler
{
public:
virtual int32_t write(GWBUF* pBuffer) = 0;
};
/**
* Constructor
*
* @param pSession The session object of the DCB.
* @param zUser The client of the connection.
* @param zHost The host of the connection.
* @param pHandler Optional handler.
*/
Dcb(MXS_SESSION* pSession,
const char* zUser,
const char* zHost,
Handler* pHandler = NULL);
~Dcb();
/**
* Get the current handler of the Dcb.
*
* @return A Handler or NULL.
*/
Handler* handler() const;
/**
* Set the current handler of the Dcb.
*
* @param pHandler The new handler.
*
* @return The previous handler or NULL.
*/
Handler* set_handler(Handler* pHandler);
private:
int32_t write(GWBUF* pData);
static int32_t write(DCB* pDcb, GWBUF* pData);
private:
std::string m_user;
std::string m_host;
Handler* m_pHandler;
};
}
}

View File

@ -0,0 +1,54 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <string>
#include <string.h>
#include <maxscale/buffer.h>
#include <maxscale/modutil.h>
namespace maxscale
{
namespace mock
{
/**
* Create a COM_QUERY packet containing the provided statement.
*
* @param zStatement Null terminated string containing an SQL statement.
*
* @return A buffer containing a COM_QUERY packet.
*/
inline GWBUF* create_com_query(const char* zStatement)
{
return modutil_create_query(zStatement);
}
/**
* Create a COM_QUERY packet containing the provided statement.
*
* @param statement String containing an SQL statement.
*
* @return A buffer containing a COM_QUERY packet.
*/
inline GWBUF* create_com_query(const std::string& statement)
{
return create_com_query(statement.c_str());
}
}
}

View File

@ -0,0 +1,106 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <memory>
#include <deque>
#include <maxscale/router.hh>
#include "../filtermodule.hh"
#include "mock.hh"
namespace maxscale
{
namespace mock
{
class Backend;
/**
* An instance of RouterSession is a router to which a filter forwards
* data.
*/
class RouterSession : private MXS_ROUTER_SESSION
{
RouterSession(const RouterSession&);
RouterSession& operator = (const RouterSession&);
public:
/**
* Constructor
*
* @param pBackend The backend associated with the router.
*/
RouterSession(Backend* pBackend);
~RouterSession();
/**
* Set the router as the downstream filter of a particular filter.
* The filter will at the same time become the upstream filter of
* this router.
*
* @param pFilter_session The filter to set this router as downstream
* filter of.
*/
void set_as_downstream_on(FilterModule::Session* pFilter_session);
/**
* Called by the backend to deliver a response.
*
* @return Whatever the upstream filter returns.
*/
int32_t clientReply(GWBUF* pResponse);
/**
* Causes the router to make its associated backend deliver a response
* to this router, which will then deliver it forward to its associated
* upstream filter.
*
* @return True if there are additional responses to deliver.
*/
bool respond();
/**
* Are there responses available.
*
* @return True, if there are no responses, false otherwise.
*/
bool idle() const;
/**
* Discards one response.
*
* @return True, if there are additional responses.
*/
bool discard_one_response();
/**
* Discards all responses.
*/
void discard_all_responses();
private:
int32_t routeQuery(MXS_ROUTER* pInstance, GWBUF* pStatement);
static int32_t routeQuery(MXS_FILTER* pInstance, MXS_FILTER_SESSION* pRouter_session, GWBUF* pStatement);
private:
MXS_ROUTER m_instance;
Backend* m_pBackend;
FilterModule::Session* m_pUpstream_filter_session;
};
}
}

View File

@ -0,0 +1,55 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "mock.hh"
#include <maxscale/session.h>
#include <maxscale/protocol/mysql.h>
#include "client.hh"
namespace maxscale
{
namespace mock
{
/**
* The class Session provides a mock MXS_SESSION that can be used when
* testing.
*/
class Session : public MXS_SESSION
{
Session(const Session&);
Session& operator = (Session&);
public:
/**
* Constructor
*
* @param pClient The client of the session. Must remain valid for
* the lifetime of the Session.
*/
Session(Client* pClient);
~Session();
Client& client() const;
private:
Client& m_client;
Dcb m_client_dcb;
MYSQL_session m_mysql_session;
};
}
}

View File

@ -0,0 +1,100 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <memory>
namespace maxscale
{
/**
* The class Module is an abstraction for a MaxScale module, to
* be used as the base class of a specific module.
*/
class Module
{
public:
/**
* Load a module with a specific name, assumed to be of a specific type.
*
* @param zFile_name The name of the module.
* @param zType_name The expected type of the module.
*
* @return The module object, if the module could be loaded, otherwise NULL.
*/
static void* load(const char *zFile_name, const char *zType_name);
/**
* Perform process initialization of all modules. Should be called only
* when all modules intended to be loaded have been loaded.
*
* @return True, if the process initialization succeeded.
*/
static bool process_init();
/**
* Perform process finalization of all modules.
*/
static void process_finish();
/**
* Perform thread initialization of all modules. Should be called only
* when all modules intended to be loaded have been loaded.
*
* @return True, if the thread initialization could be performed.
*/
static bool thread_init();
/**
* Perform thread finalization of all modules.
*/
static void thread_finish();
};
/**
* The template Module is intended to be derived from using the derived
* class as template argument.
*
* class XyzModule : public SpecificModule<XyzModule> { ... }
*
* @param zFile_name The name of the module.
*
* @return A module instance if the module could be loaded and it was of
* the expected type.
*/
template<class T>
class SpecificModule : public Module
{
public:
static std::auto_ptr<T> load(const char* zFile_name)
{
std::auto_ptr<T> sT;
void* pApi = Module::load(zFile_name, T::zName);
if (pApi)
{
sT.reset(new T(static_cast<typename T::type_t*>(pApi)));
}
return sT;
}
protected:
SpecificModule()
{
}
};
}

View File

@ -0,0 +1,48 @@
#pragma once
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <maxscale/cppdefs.hh>
#include <memory>
#include <maxscale/query_classifier.h>
#include "module.hh"
namespace maxscale
{
/**
* A QueryClassfierModule instance is an abstraction for a query
* classifier module.
*/
class QueryClassifierModule : public SpecificModule<QueryClassifierModule>
{
QueryClassifierModule(const QueryClassifierModule&);
QueryClassifierModule& operator = (const QueryClassifierModule&);
public:
static const char* zName; /*< The name describing the module type. */
typedef QUERY_CLASSIFIER type_t; /*< The type of the module object. */
private:
friend class SpecificModule<QueryClassifierModule>;
QueryClassifierModule(QUERY_CLASSIFIER* pApi)
: m_pApi(pApi)
{
}
private:
QUERY_CLASSIFIER* m_pApi;
};
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/mock/mock.hh"
#include <maxscale/protocol/mysql.h>
namespace maxscale
{
namespace mock
{
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/mock/backend.hh"
#include <algorithm>
#include <maxscale/protocol/mysql.h>
#include <iostream>
using namespace std;
namespace maxscale
{
namespace mock
{
//
// Backend
//
Backend::Backend()
{
}
Backend::~Backend()
{
}
//
// BufferBackend
//
BufferBackend::BufferBackend()
{
}
BufferBackend::~BufferBackend()
{
}
bool BufferBackend::respond(RouterSession* pSession)
{
bool empty = false;
GWBUF* pResponse = dequeue_response(pSession, &empty);
if (pResponse)
{
pSession->clientReply(pResponse);
}
return !empty;
}
bool BufferBackend::idle(const RouterSession* pSession) const
{
bool rv = true;
SessionResponses::const_iterator i = m_session_responses.find(pSession);
if (i != m_session_responses.end())
{
const Responses& responses = i->second;
rv = responses.empty();
}
return rv;
}
bool BufferBackend::discard_one_response(const RouterSession* pSession)
{
bool empty = false;
gwbuf_free(dequeue_response(pSession, &empty));
return !empty;
}
void BufferBackend::discard_all_responses(const RouterSession* pSession)
{
ss_dassert(!idle(pSession));
if (!idle(pSession))
{
Responses& responses = m_session_responses[pSession];
ss_dassert(!responses.empty());
std::for_each(responses.begin(), responses.end(), gwbuf_free);
responses.clear();
}
}
void BufferBackend::enqueue_response(const RouterSession* pSession, GWBUF* pResponse)
{
Responses& responses = m_session_responses[pSession];
responses.push_back(pResponse);
}
GWBUF* BufferBackend::dequeue_response(const RouterSession* pSession, bool* pEmpty)
{
ss_dassert(!idle(pSession));
GWBUF* pResponse = NULL;
*pEmpty = true;
if (!idle(pSession))
{
Responses& responses = m_session_responses[pSession];
ss_dassert(!responses.empty());
if (!responses.empty())
{
pResponse = responses.front();
responses.pop_front();
}
*pEmpty = responses.empty();
}
return pResponse;
}
//
// OkBackend
//
OkBackend::OkBackend()
{
}
void OkBackend::handle_statement(RouterSession* pSession, GWBUF* pStatement)
{
/* Note: sequence id is always 01 (4th byte) */
const static uint8_t ok[MYSQL_OK_PACKET_MIN_LEN] =
{ 07, 00, 00, 01, 00, 00, 00, 02, 00, 00, 00 };
GWBUF* pResponse = gwbuf_alloc_and_load(sizeof(ok), &ok);
ss_dassert(pResponse);
enqueue_response(pSession, pResponse);
gwbuf_free(pStatement);
}
} // mock
} // maxscale

View File

@ -0,0 +1,143 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/mock/client.hh"
namespace maxscale
{
namespace mock
{
//
// Client
//
Client::Client(const char* zUser,
const char* zHost,
Handler* pHandler)
: m_user(zUser)
, m_host(zHost)
, m_pHandler(pHandler)
, m_n_responses(0)
{
}
Client::~Client()
{
}
const char* Client::user() const
{
return m_user.c_str();
}
const char* Client::host() const
{
return m_host.c_str();
}
size_t Client::n_responses() const
{
return m_n_responses;
}
Client::Handler* Client::set_handler(Handler* pHandler)
{
Handler* pH = m_pHandler;
m_pHandler = pHandler;
return pH;
}
void Client::reset()
{
m_n_responses = 0;
if (m_pHandler)
{
m_pHandler->reset();
}
}
void Client::set_as_upstream_on(FilterModule::Session& filter_session)
{
MXS_UPSTREAM upstream;
upstream.instance = &m_instance;
upstream.session = this;
upstream.clientReply = &Client::clientReply;
upstream.error = NULL;
filter_session.setUpstream(&upstream);
}
int32_t Client::clientReply(GWBUF* pResponse)
{
int32_t rv = 1;
++m_n_responses;
if (m_pHandler)
{
rv = m_pHandler->backend_reply(pResponse);
}
else
{
gwbuf_free(pResponse);
}
return rv;
}
int32_t Client::write(GWBUF* pResponse)
{
int32_t rv = 1;
++m_n_responses;
if (m_pHandler)
{
rv = m_pHandler->maxscale_reply(pResponse);
}
else
{
gwbuf_free(pResponse);
}
return rv;
}
//static
int32_t Client::clientReply(MXS_FILTER* pInstance,
MXS_FILTER_SESSION* pSession,
GWBUF* pResponse)
{
Client* pClient = reinterpret_cast<Client*>(pSession);
ss_dassert(pInstance == &pClient->m_instance);
return pClient->clientReply(pResponse);
}
//
// Client::Handler
//
Client::Handler::~Handler()
{
}
void Client::Handler::reset()
{
}
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/mock/dcb.hh"
namespace
{
void initialize_dcb(DCB* pDcb)
{
memset(pDcb, 0, sizeof(DCB));
pDcb->dcb_chk_top = CHK_NUM_DCB;
pDcb->fd = DCBFD_CLOSED;
pDcb->state = DCB_STATE_ALLOC;
pDcb->ssl_state = SSL_HANDSHAKE_UNKNOWN;
pDcb->dcb_chk_tail = CHK_NUM_DCB;
}
}
namespace maxscale
{
namespace mock
{
Dcb::Dcb(MXS_SESSION* pSession,
const char* zUser,
const char* zHost,
Handler* pHandler)
: m_user(zUser)
, m_host(zHost)
, m_pHandler(pHandler)
{
DCB* pDcb = this;
initialize_dcb(this);
pDcb->session = pSession;
pDcb->remote = const_cast<char*>(zHost);
pDcb->user = const_cast<char*>(zUser);
pDcb->func.write = &Dcb::write;
}
Dcb::~Dcb()
{
}
Dcb::Handler* Dcb::handler() const
{
return m_pHandler;
}
Dcb::Handler* Dcb::set_handler(Handler* pHandler)
{
Handler* p = m_pHandler;
m_pHandler = pHandler;
return p;
}
int32_t Dcb::write(GWBUF* pData)
{
int32_t rv = 1;
if (m_pHandler)
{
rv = m_pHandler->write(pData);
}
else
{
gwbuf_free(pData);
}
return rv;
}
//static
int32_t Dcb::write(DCB* pDcb, GWBUF* pData)
{
return static_cast<Dcb*>(pDcb)->write(pData);
}
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/mock/routersession.hh"
#include "maxscale/mock/backend.hh"
namespace maxscale
{
namespace mock
{
RouterSession::RouterSession(Backend* pBackend)
: m_pBackend(pBackend)
{
memset(&m_instance, 0, sizeof(m_instance));
}
RouterSession::~RouterSession()
{
}
void RouterSession::set_as_downstream_on(FilterModule::Session* pFilter_session)
{
MXS_DOWNSTREAM downstream;
downstream.instance = reinterpret_cast<MXS_FILTER*>(&m_instance);
downstream.session = reinterpret_cast<MXS_FILTER_SESSION*>(this);
downstream.routeQuery = &RouterSession::routeQuery;
pFilter_session->setDownstream(&downstream);
m_pUpstream_filter_session = pFilter_session;
}
bool RouterSession::respond()
{
return m_pBackend->respond(this);
}
bool RouterSession::idle() const
{
return m_pBackend->idle(this);
}
bool RouterSession::discard_one_response()
{
return m_pBackend->discard_one_response(this);
}
void RouterSession::discard_all_responses()
{
return m_pBackend->discard_all_responses(this);
}
int32_t RouterSession::routeQuery(MXS_ROUTER* pInstance, GWBUF* pStatement)
{
ss_dassert(pInstance == &m_instance);
m_pBackend->handle_statement(this, pStatement);
return 1;
}
int32_t RouterSession::clientReply(GWBUF* pResponse)
{
return m_pUpstream_filter_session->clientReply(pResponse);
}
//static
int32_t RouterSession::routeQuery(MXS_FILTER* pInstance,
MXS_FILTER_SESSION* pRouter_session,
GWBUF* pStatement)
{
RouterSession* pThis = reinterpret_cast<RouterSession*>(pRouter_session);
return pThis->routeQuery(reinterpret_cast<MXS_ROUTER*>(pInstance), pStatement);
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/mock/session.hh"
namespace maxscale
{
namespace mock
{
Session::Session(Client* pClient)
: m_client(*pClient)
, m_client_dcb(this, pClient->user(), pClient->host(), pClient)
{
MXS_SESSION* pSession = this;
memset(pSession, 0, sizeof(MXS_SESSION));
pSession->ses_chk_top = CHK_NUM_SESSION;
pSession->state = SESSION_STATE_ALLOC;
pSession->ses_chk_tail = CHK_NUM_SESSION;
pSession->client_dcb = &m_client_dcb;
memset(&m_mysql_session, 0, sizeof(m_mysql_session));
strcpy(m_mysql_session.db, "dummy");
m_client_dcb.data = &m_mysql_session;
}
Session::~Session()
{
}
Client& Session::client() const
{
return m_client;
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/module.hh"
#include "../../../core/internal/modules.h"
namespace maxscale
{
//static
void* Module::load(const char* zName, const char* zType)
{
return load_module(zName, zType);
}
//static
bool Module::process_init()
{
bool initialized = false;
MXS_MODULE_ITERATOR i = mxs_module_iterator_get(NULL);
MXS_MODULE* module = NULL;
while ((module = mxs_module_iterator_get_next(&i)) != NULL)
{
if (module->process_init)
{
int rc = (module->process_init)();
if (rc != 0)
{
break;
}
}
}
if (module)
{
// If module is non-NULL it means that the initialization failed for
// that module. We now need to call finish on all modules that were
// successfully initialized.
MXS_MODULE* failed_module = module;
i = mxs_module_iterator_get(NULL);
while ((module = mxs_module_iterator_get_next(&i)) != failed_module)
{
if (module->process_finish)
{
(module->process_finish)();
}
}
}
else
{
initialized = true;
}
return initialized;
}
//static
void Module::process_finish()
{
MXS_MODULE_ITERATOR i = mxs_module_iterator_get(NULL);
MXS_MODULE* module = NULL;
while ((module = mxs_module_iterator_get_next(&i)) != NULL)
{
if (module->process_finish)
{
(module->process_finish)();
}
}
}
//static
bool Module::thread_init()
{
bool initialized = false;
MXS_MODULE_ITERATOR i = mxs_module_iterator_get(NULL);
MXS_MODULE* module = NULL;
while ((module = mxs_module_iterator_get_next(&i)) != NULL)
{
if (module->thread_init)
{
int rc = (module->thread_init)();
if (rc != 0)
{
break;
}
}
}
if (module)
{
// If module is non-NULL it means that the initialization failed for
// that module. We now need to call finish on all modules that were
// successfully initialized.
MXS_MODULE* failed_module = module;
i = mxs_module_iterator_get(NULL);
while ((module = mxs_module_iterator_get_next(&i)) != failed_module)
{
if (module->thread_finish)
{
(module->thread_finish)();
}
}
}
else
{
initialized = true;
}
return initialized;
}
//static
void Module::thread_finish()
{
MXS_MODULE_ITERATOR i = mxs_module_iterator_get(NULL);
MXS_MODULE* module = NULL;
while ((module = mxs_module_iterator_get_next(&i)) != NULL)
{
if (module->thread_finish)
{
(module->thread_finish)();
}
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include "maxscale/queryclassifiermodule.hh"
#include "../../../core/internal/modules.h"
namespace maxscale
{
//static
const char* QueryClassifierModule::zName = MODULE_QUERY_CLASSIFIER;
}