From af2ee554c2f3da6a948d8de6e370adf69145dc92 Mon Sep 17 00:00:00 2001 From: Joakim Hansson Date: Sun, 1 Oct 2017 02:43:20 +0200 Subject: [PATCH 1/3] Add helper method for creating parameterized queries - Added string extension to sanitize queries. --- .../Helpers/StringExtensions.cs | 32 +++++++ .../Helpers/QueryHelpers.cs | 52 +++++++++++ .../InfluxData.Net.Tests.csproj | 3 + .../InfluxDb/Helpers/QueryHelpersTests.cs | 92 +++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 InfluxData.Net.Common/Helpers/StringExtensions.cs create mode 100644 InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs create mode 100644 InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs diff --git a/InfluxData.Net.Common/Helpers/StringExtensions.cs b/InfluxData.Net.Common/Helpers/StringExtensions.cs new file mode 100644 index 0000000..5cd64d1 --- /dev/null +++ b/InfluxData.Net.Common/Helpers/StringExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.RegularExpressions; + +namespace InfluxData.Net.Common +{ + public static class StringExtensions + { + // http://www.mvvm.ro/2011/03/sanitize-strings-against-sql-injection.html + public static string Sanitize(this string stringValue) + { + if (null == stringValue) + return stringValue; + return stringValue + .RegexReplace("-{2,}", "-") + .RegexReplace(@"[*/]+", string.Empty) + .RegexReplace(@"(;|\s)(exec|execute|select|insert|update|delete|create|alter|drop|rename|truncate|backup|restore)\s", + string.Empty, RegexOptions.IgnoreCase); + } + + + private static string RegexReplace(this string stringValue, string matchPattern, string toReplaceWith) + { + return Regex.Replace(stringValue, matchPattern, toReplaceWith); + } + + private static string RegexReplace(this string stringValue, string matchPattern, string toReplaceWith, RegexOptions regexOptions) + { + return Regex.Replace(stringValue, matchPattern, toReplaceWith, regexOptions); + } + + } +} diff --git a/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs b/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs new file mode 100644 index 0000000..cff5c8d --- /dev/null +++ b/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using InfluxData.Net.Common; +using System.Linq; + +namespace InfluxData.Net.InfluxDb.Helpers +{ + public static class QueryHelpers + { + public static string BuildParameterizedQuery(string query, object param) + { + var paramRegex = "@([A-Za-z0-9åäöÅÄÖ'_-]+)"; + + var matches = Regex.Matches(query, paramRegex); + + Type t = param.GetType(); + PropertyInfo[] pi = t.GetProperties(); + + foreach(Match match in matches) + { + if (!pi.Any(x => match.Groups[0].Value.Contains(x.Name))) + throw new ArgumentException($"Missing parameter value for {match.Groups[0].Value}"); + } + + foreach (var propertyInfo in pi) + { + var paramValue = propertyInfo.GetValue(param); + var paramType = paramValue.GetType(); + + if(!paramType.IsPrimitive && paramType != typeof(String)) + throw new NotSupportedException($"The type {paramType.Name} is not a supported query parameter type."); + + var sanitizedParamValue = paramValue; + + if(paramType == typeof(String)) { + sanitizedParamValue = ((string)sanitizedParamValue).Sanitize(); + } + + while (Regex.IsMatch(query, $"@{propertyInfo.Name}")) + { + var match = Regex.Match(query, $"@{propertyInfo.Name}"); + + query = query.Remove(match.Index, match.Length); + query = query.Insert(match.Index, $"{sanitizedParamValue}"); + } + } + + return query; + } + } +} \ No newline at end of file diff --git a/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj b/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj index 09b2553..bb40465 100644 --- a/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj +++ b/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj @@ -32,4 +32,7 @@ PreserveNewest + + + \ No newline at end of file diff --git a/InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs b/InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs new file mode 100644 index 0000000..981243d --- /dev/null +++ b/InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using InfluxData.Net.InfluxDb.Helpers; +using Xunit; + +namespace InfluxData.Net.Tests +{ + [Trait("InfluxDb SerieExtensions", "Serie extensions")] + public class QueryHelpersTests + { + [Fact] + public void Building_Parameterized_Query_Returns_Correct_String() + { + var firstTag = "firstTag"; + var firstTagValue = "firstTagValue"; + + var firstField = "firstField"; + var firstFieldValue = "firstFieldValue"; + + var query = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = @FirstTagValue " + + $"AND {firstField} = @FirstFieldValue"; + + var expectedNewQuery = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = {firstTagValue} " + + $"AND {firstField} = {firstFieldValue}"; + + var actualNewQuery = QueryHelpers.BuildParameterizedQuery( + query, + new + { + @FirstTagValue = firstTagValue, + @FirstFieldValue = firstFieldValue + }); + + Assert.Equal(expectedNewQuery, actualNewQuery); + } + + + [Fact] + public void Using_Non_Primitive_And_Non_String_Type_In_Parameters_Throws_NotSupportedException() + { + var firstTag = "firstTag"; + var firstTagValue = "firstTagValue"; + + var firstField = "firstField"; + + var query = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = @FirstTagValue " + + $"AND {firstField} = @FirstFieldValue"; + + Func func = new Func(() => + { + return QueryHelpers.BuildParameterizedQuery( + query, + new + { + @FirstTagValue = firstTagValue, + @FirstFieldValue = new List() { "NOT ACCEPTED" } + }); + }); + + Assert.Throws(typeof(NotSupportedException), func); + } + + [Fact] + public void Building_Parameterized_Query_With_Missing_Parameters_Throws_ArgumentException() + { + var firstTag = "firstTag"; + var firstTagValue = "firstTagValue"; + + var firstField = "firstField"; + + var query = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = @FirstTagValue " + + $"AND {firstField} = @FirstFieldValue"; + + Func func = new Func(() => + { + return QueryHelpers.BuildParameterizedQuery( + query, + new + { + @FirstTagValue = firstTagValue + }); + }); + + Assert.Throws(typeof(ArgumentException), func); + } + } +} From 24c71186272d4c83835e148f51297b2844817f8c Mon Sep 17 00:00:00 2001 From: Joakim Hansson Date: Sun, 1 Oct 2017 02:44:23 +0200 Subject: [PATCH 2/3] Add QueryAsync overload to allow for parameterized queries - Add integration tests --- .../ClientModules/BasicClientModule.cs | 9 ++++++ .../ClientModules/IBasicClientModule.cs | 12 +++++++ .../InfluxDb/Tests/IntegrationBasic.cs | 32 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs b/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs index 1ce485e..e963ed0 100644 --- a/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs +++ b/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs @@ -7,6 +7,8 @@ using InfluxData.Net.InfluxDb.RequestClients; using InfluxData.Net.InfluxDb.ResponseParsers; using InfluxData.Net.Common.Constants; +using InfluxData.Net.InfluxDb.Helpers; +using System; namespace InfluxData.Net.InfluxDb.ClientModules { @@ -14,6 +16,13 @@ public class BasicClientModule : ClientModuleBase, IBasicClientModule { private readonly IBasicResponseParser _basicResponseParser; + public Task> QueryAsync(string query, object param = null, string dbName = null, string epochFormat = null, long? chunkSize = default(long?)) + { + var buildQuery = QueryHelpers.BuildParameterizedQuery(query, param); + + return this.QueryAsync(buildQuery, dbName, epochFormat, chunkSize); + } + public virtual async Task> QueryAsync(string query, string dbName = null, string epochFormat = null, long? chunkSize = null) { var series = await base.ResolveSingleGetSeriesResultAsync(query, dbName, epochFormat, chunkSize).ConfigureAwait(false); diff --git a/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs b/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs index 2c11b08..d5ba931 100644 --- a/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs +++ b/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs @@ -9,6 +9,18 @@ namespace InfluxData.Net.InfluxDb.ClientModules { public interface IBasicClientModule { + /// + /// Executes a parameterized query against the database. If chunkSize is specified, responses + /// will be broken down by number of returned rows. + /// + /// Query to execute. + /// The parameters to pass, if any. (OPTIONAL) + /// Database name. (OPTIONAL) + /// Epoch timestamp format. (OPTIONAL) + /// Maximum number of rows per chunk. (OPTIONAL) + /// + Task> QueryAsync(string query, object param = null, string dbName = null, string epochFormat = null, long? chunkSize = null); + /// /// Executes a query against the database. If chunkSize is specified, responses /// will be broken down by number of returned rows. diff --git a/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs b/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs index dd2938f..a553968 100644 --- a/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs +++ b/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs @@ -225,6 +225,38 @@ public virtual async Task ClientQuery_OnExistingPoints_ShouldReturnSerieCollecti result.First().Values.Should().HaveCount(3); } + [Fact] + public virtual async Task ClientQuery_Parameterized_OnExistingPoints_ShouldReturnSerieCollection() + { + var points = await _fixture.MockAndWritePoints(3); + + var firstTag = points.First().Tags.First().Key; + var firstTagValue = points.First().Tags.First().Value; + + var firstField = points.First().Fields.First().Key; + var firstFieldValue = points.First().Fields.First().Value; + + var query = $"SELECT * FROM {points.First().Name} " + + $@"WHERE {firstTag} = '@FirstTagValueParam' " + + $@"AND {firstField} = @FirstFieldValueParam"; + + + var result = await _fixture.Sut.Client.QueryAsync( + query, + new + { + @FirstTagValueParam = firstTagValue, + @FirstFieldValueParam = firstFieldValue + }, _fixture.DbName); + + var t = result.First(); + + result.Should().NotBeNull(); + result.Should().HaveCount(1); + result.First().Name.Should().Be(points.First().Name); + result.First().Values.Should().HaveCount(1); + } + [Fact] public virtual async Task ClientQueryMultiple_OnExistingPoints_ShouldReturnSerieCollection() { From 0d12aba7ca977ca5a8f080ff9d228d1ee5927f84 Mon Sep 17 00:00:00 2001 From: Joakim Hansson Date: Mon, 2 Oct 2017 20:26:57 +0200 Subject: [PATCH 3/3] Correct regex to match only full words --- .../Helpers/QueryHelpers.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs b/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs index cff5c8d..f51ed79 100644 --- a/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs +++ b/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs @@ -10,36 +10,36 @@ public static class QueryHelpers { public static string BuildParameterizedQuery(string query, object param) { - var paramRegex = "@([A-Za-z0-9åäöÅÄÖ'_-]+)"; - - var matches = Regex.Matches(query, paramRegex); - Type t = param.GetType(); PropertyInfo[] pi = t.GetProperties(); - foreach(Match match in matches) - { - if (!pi.Any(x => match.Groups[0].Value.Contains(x.Name))) - throw new ArgumentException($"Missing parameter value for {match.Groups[0].Value}"); - } foreach (var propertyInfo in pi) { + var regex = $@"@{propertyInfo.Name}(?!\w)"; + + if(!Regex.IsMatch(query, regex) && Nullable.GetUnderlyingType(propertyInfo.GetType()) != null) + throw new ArgumentException($"Missing parameter identifier for @{propertyInfo.Name}"); + var paramValue = propertyInfo.GetValue(param); + if (paramValue == null) + continue; + var paramType = paramValue.GetType(); - if(!paramType.IsPrimitive && paramType != typeof(String)) + if (!paramType.IsPrimitive && paramType != typeof(String) && paramType != typeof(DateTime)) throw new NotSupportedException($"The type {paramType.Name} is not a supported query parameter type."); var sanitizedParamValue = paramValue; - if(paramType == typeof(String)) { + if (paramType == typeof(String)) + { sanitizedParamValue = ((string)sanitizedParamValue).Sanitize(); } - while (Regex.IsMatch(query, $"@{propertyInfo.Name}")) + while (Regex.IsMatch(query, regex)) { - var match = Regex.Match(query, $"@{propertyInfo.Name}"); + var match = Regex.Match(query, regex); query = query.Remove(match.Index, match.Length); query = query.Insert(match.Index, $"{sanitizedParamValue}"); @@ -49,4 +49,4 @@ public static string BuildParameterizedQuery(string query, object param) return query; } } -} \ No newline at end of file +}