[asterisk-commits] realtime: Fix LIKE escaping in SQL backends (asterisk[13])

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Tue Feb 21 06:17:34 CST 2017


Anonymous Coward #1000019 has submitted this change and it was merged. ( https://gerrit.asterisk.org/4978 )

Change subject: realtime: Fix LIKE escaping in SQL backends
......................................................................


realtime: Fix LIKE escaping in SQL backends

The realtime framework allows for components to look up values using a
LIKE clause with similar syntax to SQL's. pbx_realtime uses this
functionality to search for pattern matching extensions that start with
an underscore (_).

When passing an underscore to SQL's LIKE clause, it will be interpreted
as a wildcard matching a single character and therefore needs to be
escaped. It is (for better or for worse) the responsibility of the
component that is querying realtime to escape it with a backslash before
passing it in. Some RDBMs support escape characters by default, but the
SQL92 standard explicitly says that there are no escape characters
unless they are specified with an ESCAPE clause, e.g.

	SELECT * FROM table WHERE column LIKE '\_%' ESCAPE '\'

This patch instructs 3 backends - res_config_mysql, res_config_pgsql,
and res_config_sqlite3 - to use the ESCAPE clause where appropriate.

Looking through documentation and source tarballs, I was able to
determine that the ESCAPE clause is supported in:

MySQL 5.0.15   (released 2005-10-22 - earliest version available from
                archives)
PostgreSQL 7.1 (released 2001-04-13)
SQLite 3.1.0   (released 2005-01-21)

The versions of the relevant libraries that we depend on to access MySQL
and PostgreSQL will not work on versions that old, and I've added an
explicit check in res_config_sqlite3 to only use the ESCAPE clause when
we have a sufficiently new version of SQLite3.

res_config_odbc already handles the escape characters appropriately, so
no changes were required there.

ASTERISK-15858 #close
Reported by: Humberto Figuera

ASTERISK-26057 #close
Reported by: Stepan

Change-Id: I93117fbb874189ae819f4a31222df7c82cd20efa
---
M addons/res_config_mysql.c
M res/res_config_pgsql.c
M res/res_config_sqlite3.c
3 files changed, 118 insertions(+), 24 deletions(-)

Approvals:
  Kevin Harwell: Looks good to me, but someone else must approve
  Anonymous Coward #1000019: Verified
  Joshua Colp: Looks good to me, approved



diff --git a/addons/res_config_mysql.c b/addons/res_config_mysql.c
index 1041c11..b9ca81f 100644
--- a/addons/res_config_mysql.c
+++ b/addons/res_config_mysql.c
@@ -305,6 +305,11 @@
 	return orig;
 }
 
+#define IS_SQL_LIKE_CLAUSE(x) ((x) && ast_ends_with(x, " LIKE"))
+
+/* MySQL requires us to escape the escape... yo dawg */
+static char *ESCAPE_CLAUSE = " ESCAPE '\\\\'";
+
 static struct ast_variable *realtime_mysql(const char *database, const char *table, const struct ast_variable *rt_fields)
 {
 	struct mysql_conn *dbh;
@@ -317,6 +322,7 @@
 	char *stringp;
 	char *chunk;
 	char *op;
+	char *escape = "";
 	const struct ast_variable *field = rt_fields;
 	struct ast_variable *var=NULL, *prev=NULL;
 
@@ -347,20 +353,29 @@
 	/* Create the first part of the query using the first parameter/value pairs we just extracted
 	   If there is only 1 set, then we have our query. Otherwise, loop thru the list and concat */
 
-	if (!strchr(field->name, ' ')) 
-		op = " ="; 
-	else 
+	if (!strchr(field->name, ' ')) {
+		op = " =";
+	} else {
 		op = "";
+		if (IS_SQL_LIKE_CLAUSE(field->name)) {
+			escape = ESCAPE_CLAUSE;
+		}
+	}
 
 	ESCAPE_STRING(buf, field->value);
-	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'", table, field->name, op, ast_str_buffer(buf));
+	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'%s", table, field->name, op, ast_str_buffer(buf), escape);
 	while ((field = field->next)) {
-		if (!strchr(field->name, ' ')) 
-			op = " ="; 
-		else
+		escape = "";
+		if (!strchr(field->name, ' ')) {
+			op = " =";
+		} else {
 			op = "";
+			if (IS_SQL_LIKE_CLAUSE(field->name)) {
+				escape = ESCAPE_CLAUSE;
+			}
+		}
 		ESCAPE_STRING(buf, field->value);
-		ast_str_append(&sql, 0, " AND %s%s '%s'", field->name, op, ast_str_buffer(buf));
+		ast_str_append(&sql, 0, " AND %s%s '%s'%s", field->name, op, ast_str_buffer(buf), escape);
 	}
 
 	ast_debug(1, "MySQL RealTime: Retrieve SQL: %s\n", ast_str_buffer(sql));
@@ -418,6 +433,7 @@
 	char *stringp;
 	char *chunk;
 	char *op;
+	char *escape = "";
 	const struct ast_variable *field = rt_fields;
 	struct ast_variable *var = NULL;
 	struct ast_config *cfg = NULL;
@@ -464,17 +480,29 @@
 	/* Create the first part of the query using the first parameter/value pairs we just extracted
 	   If there is only 1 set, then we have our query. Otherwise, loop thru the list and concat */
 
-	if (!strchr(field->name, ' '))
+	if (!strchr(field->name, ' ')) {
 		op = " =";
-	else
+	} else {
 		op = "";
+		if (IS_SQL_LIKE_CLAUSE(field->name)) {
+			escape = ESCAPE_CLAUSE;
+		}
+	}
 
 	ESCAPE_STRING(buf, field->value);
-	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'", table, field->name, op, ast_str_buffer(buf));
+	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'%s", table, field->name, op, ast_str_buffer(buf), escape);
 	while ((field = field->next)) {
-		if (!strchr(field->name, ' ')) op = " ="; else op = "";
+		escape = "";
+		if (!strchr(field->name, ' ')) {
+			op = " =";
+		} else {
+			op = "";
+			if (IS_SQL_LIKE_CLAUSE(field->name)) {
+				escape = ESCAPE_CLAUSE;
+			}
+		}
 		ESCAPE_STRING(buf, field->value);
-		ast_str_append(&sql, 0, " AND %s%s '%s'", field->name, op, ast_str_buffer(buf));
+		ast_str_append(&sql, 0, " AND %s%s '%s'%s", field->name, op, ast_str_buffer(buf), escape);
 	}
 
 	if (initfield) {
diff --git a/res/res_config_pgsql.c b/res/res_config_pgsql.c
index 52c8ede..58c34d0 100644
--- a/res/res_config_pgsql.c
+++ b/res/res_config_pgsql.c
@@ -417,6 +417,9 @@
 	return NULL;
 }
 
+#define IS_SQL_LIKE_CLAUSE(x) ((x) && ast_ends_with(x, " LIKE"))
+static char *ESCAPE_CLAUSE = " ESCAPE '\\'";
+
 static struct ast_variable *realtime_pgsql(const char *database, const char *tablename, const struct ast_variable *fields)
 {
 	RAII_VAR(PGresult *, result, NULL, PQclear);
@@ -426,6 +429,7 @@
 	char *stringp;
 	char *chunk;
 	char *op;
+	char *escape = "";
 	const struct ast_variable *field = fields;
 	struct ast_variable *var = NULL, *prev = NULL;
 
@@ -453,7 +457,14 @@
 
 	/* Create the first part of the query using the first parameter/value pairs we just extracted
 	   If there is only 1 set, then we have our query. Otherwise, loop thru the list and concat */
-	op = strchr(field->name, ' ') ? "" : " =";
+	if (!strchr(field->name, ' ')) {
+		op = " =";
+	} else {
+		op = "";
+		if (IS_SQL_LIKE_CLAUSE(field->name)) {
+			escape = ESCAPE_CLAUSE;
+		}
+	}
 
 	ESCAPE_STRING(escapebuf, field->value);
 	if (pgresult) {
@@ -461,12 +472,17 @@
 		return NULL;
 	}
 
-	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'", tablename, field->name, op, ast_str_buffer(escapebuf));
+	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'%s", tablename, field->name, op, ast_str_buffer(escapebuf), escape);
 	while ((field = field->next)) {
-		if (!strchr(field->name, ' '))
+		escape = "";
+		if (!strchr(field->name, ' ')) {
 			op = " =";
-		else
+		} else {
 			op = "";
+			if (IS_SQL_LIKE_CLAUSE(field->name)) {
+				escape = ESCAPE_CLAUSE;
+			}
+		}
 
 		ESCAPE_STRING(escapebuf, field->value);
 		if (pgresult) {
@@ -474,7 +490,7 @@
 			return NULL;
 		}
 
-		ast_str_append(&sql, 0, " AND %s%s '%s'", field->name, op, ast_str_buffer(escapebuf));
+		ast_str_append(&sql, 0, " AND %s%s '%s'%s", field->name, op, ast_str_buffer(escapebuf), escape);
 	}
 
 	/* We now have our complete statement; Lets connect to the server and execute it. */
@@ -540,6 +556,7 @@
 	char *stringp;
 	char *chunk;
 	char *op;
+	char *escape = "";
 	struct ast_variable *var = NULL;
 	struct ast_config *cfg = NULL;
 	struct ast_category *cat = NULL;
@@ -578,10 +595,15 @@
 	/* Create the first part of the query using the first parameter/value pairs we just extracted
 	   If there is only 1 set, then we have our query. Otherwise, loop thru the list and concat */
 
-	if (!strchr(field->name, ' '))
+	if (!strchr(field->name, ' ')) {
 		op = " =";
-	else
+		escape = "";
+	} else {
 		op = "";
+		if (IS_SQL_LIKE_CLAUSE(field->name)) {
+			escape = ESCAPE_CLAUSE;
+		}
+	}
 
 	ESCAPE_STRING(escapebuf, field->value);
 	if (pgresult) {
@@ -590,12 +612,18 @@
 		return NULL;
 	}
 
-	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'", table, field->name, op, ast_str_buffer(escapebuf));
+	ast_str_set(&sql, 0, "SELECT * FROM %s WHERE %s%s '%s'%s", table, field->name, op, ast_str_buffer(escapebuf), escape);
 	while ((field = field->next)) {
-		if (!strchr(field->name, ' '))
+		escape = "";
+		if (!strchr(field->name, ' ')) {
 			op = " =";
-		else
+			escape = "";
+		} else {
 			op = "";
+			if (IS_SQL_LIKE_CLAUSE(field->name)) {
+				escape = ESCAPE_CLAUSE;
+			}
+		}
 
 		ESCAPE_STRING(escapebuf, field->value);
 		if (pgresult) {
@@ -604,7 +632,7 @@
 			return NULL;
 		}
 
-		ast_str_append(&sql, 0, " AND %s%s '%s'", field->name, op, ast_str_buffer(escapebuf));
+		ast_str_append(&sql, 0, " AND %s%s '%s'%s", field->name, op, ast_str_buffer(escapebuf), escape);
 	}
 
 	if (initfield) {
diff --git a/res/res_config_sqlite3.c b/res/res_config_sqlite3.c
index 4c4b820..ee723d3 100644
--- a/res/res_config_sqlite3.c
+++ b/res/res_config_sqlite3.c
@@ -60,6 +60,8 @@
 /*** DOCUMENTATION
  ***/
 
+static int has_explicit_like_escaping;
+
 static struct ast_config *realtime_sqlite3_load(const char *database, const char *table, const char *configfile, struct ast_config *config, struct ast_flags flags, const char *suggested_include_file, const char *who_asked);
 static struct ast_variable *realtime_sqlite3(const char *database, const char *table, const struct ast_variable *fields);
 static struct ast_config *realtime_sqlite3_multi(const char *database, const char *table, const struct ast_variable *fields);
@@ -660,6 +662,8 @@
 	return config;
 }
 
+#define IS_SQL_LIKE_CLAUSE(x) ((x) && ast_ends_with(x, " LIKE"))
+
 /*! \brief Helper function for single and multi-row realtime load functions */
 static int realtime_sqlite3_helper(const char *database, const char *table, const struct ast_variable *fields, int is_multi, void *arg)
 {
@@ -684,6 +688,15 @@
 		} else {
 			ast_str_append(&sql, 0, " AND %s %s", sqlite3_escape_column_op(field->name),
 					sqlite3_escape_value(field->value));
+		}
+
+		if (has_explicit_like_escaping && IS_SQL_LIKE_CLAUSE(field->name)) {
+			/*
+			 * The realtime framework is going to pre-escape these
+			 * for us with a backslash. We just need to make sure
+			 * to tell SQLite about it
+			 */
+			ast_str_append(&sql, 0, " ESCAPE '\\'");
 		}
 	}
 
@@ -1184,6 +1197,29 @@
 	return 0;
 }
 
+static void discover_sqlite3_caps(void)
+{
+	/*
+	 * So we cheat a little bit here. SQLite3 added support for the
+	 * 'ESCAPE' keyword in 3.1.0. They added SQLITE_VERSION_NUMBER
+	 * in 3.1.2. So if we run into 3.1.0 or 3.1.1 in the wild, we
+	 * just treat it like < 3.1.0.
+	 *
+	 * For reference: 3.1.0, 3.1.1, and 3.1.2 were all released
+	 * within 30 days of each other in Jan/Feb 2005, so I don't
+	 * imagine we'll be finding something pre-3.1.2 that often in
+	 * practice.
+	 */
+#if defined(SQLITE_VERSION_NUMBER)
+	has_explicit_like_escaping = 1;
+#else
+	has_explicit_like_escaping = 0;
+#endif
+
+	ast_debug(3, "SQLite3 has 'LIKE ... ESCAPE ...' support? %s\n",
+			has_explicit_like_escaping ? "Yes" : "No");
+}
+
 /*!
  * \brief Load the module
  *
@@ -1196,6 +1232,8 @@
  */
 static int load_module(void)
 {
+	discover_sqlite3_caps();
+
 	if (!((databases = ao2_container_alloc(DB_BUCKETS, db_hash_fn, db_cmp_fn)))) {
 		return AST_MODULE_LOAD_FAILURE;
 	}

-- 
To view, visit https://gerrit.asterisk.org/4978
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I93117fbb874189ae819f4a31222df7c82cd20efa
Gerrit-PatchSet: 1
Gerrit-Project: asterisk
Gerrit-Branch: 13
Gerrit-Owner: Sean Bright <sean.bright at gmail.com>
Gerrit-Reviewer: Anonymous Coward #1000019
Gerrit-Reviewer: Joshua Colp <jcolp at digium.com>
Gerrit-Reviewer: Kevin Harwell <kharwell at digium.com>



More information about the asterisk-commits mailing list