diff --git a/src/KubernetesClient/Kubernetes.Watch.cs b/src/KubernetesClient/Kubernetes.Watch.cs index ca992cabe..7cf801a0e 100644 --- a/src/KubernetesClient/Kubernetes.Watch.cs +++ b/src/KubernetesClient/Kubernetes.Watch.cs @@ -1,10 +1,10 @@ -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Rest; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -43,53 +43,51 @@ public partial class Kubernetes uriBuilder.Path += path; - var query = string.Empty; - + var query = new StringBuilder(); // Don't sent watch, because setting that value will cause the WatcherDelegatingHandler to kick in. That class // "eats" the first line, which is something we don't want. // query = QueryHelpers.AddQueryString(query, "watch", "true"); - - if (@continue != null) + if(@continue != null) { - query = QueryHelpers.AddQueryString(query, "continue", Uri.EscapeDataString(@continue)); + Utilities.AddQueryParameter(query, "continue", @continue); } - if (fieldSelector != null) + if (!string.IsNullOrEmpty(fieldSelector)) { - query = QueryHelpers.AddQueryString(query, "fieldSelector", Uri.EscapeDataString(fieldSelector)); + Utilities.AddQueryParameter(query, "fieldSelector", fieldSelector); } if (includeUninitialized != null) { - query = QueryHelpers.AddQueryString(query, "includeUninitialized", includeUninitialized.Value ? "true" : "false"); + Utilities.AddQueryParameter(query, "includeUninitialized", includeUninitialized.Value ? "true" : "false"); } - if (labelSelector != null) + if (!string.IsNullOrEmpty(labelSelector)) { - query = QueryHelpers.AddQueryString(query, "labelSelector", Uri.EscapeDataString(labelSelector)); + Utilities.AddQueryParameter(query, "labelSelector", labelSelector); } if (limit != null) { - query = QueryHelpers.AddQueryString(query, "limit", limit.Value.ToString()); + Utilities.AddQueryParameter(query, "limit", limit.Value.ToString()); } if (pretty != null) { - query = QueryHelpers.AddQueryString(query, "pretty", pretty.Value ? "true" : "false"); + Utilities.AddQueryParameter(query, "pretty", pretty.Value ? "true" : "false"); } if (timeoutSeconds != null) { - query = QueryHelpers.AddQueryString(query, "timeoutSeconds", timeoutSeconds.Value.ToString()); + Utilities.AddQueryParameter(query, "timeoutSeconds", timeoutSeconds.Value.ToString()); } - if (resourceVersion != null) + if (!string.IsNullOrEmpty(resourceVersion)) { - query = QueryHelpers.AddQueryString(query, "resourceVersion", resourceVersion); + Utilities.AddQueryParameter(query, "resourceVersion", resourceVersion); } - uriBuilder.Query = query; + uriBuilder.Query = query.Length == 0 ? "" : query.ToString(1, query.Length-1); // UriBuilder.Query doesn't like leading '?' chars, so trim it // Create HTTP transport objects var httpRequest = new HttpRequestMessage(HttpMethod.Get, uriBuilder.ToString()); diff --git a/src/KubernetesClient/Kubernetes.WebSocket.cs b/src/KubernetesClient/Kubernetes.WebSocket.cs index 5da20c8c8..e3adb9de3 100644 --- a/src/KubernetesClient/Kubernetes.WebSocket.cs +++ b/src/KubernetesClient/Kubernetes.WebSocket.cs @@ -1,5 +1,4 @@ using k8s.Models; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Rest; using Microsoft.Rest.Serialization; using System; @@ -14,6 +13,8 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using System.Text; +using System.Globalization; namespace k8s { @@ -102,27 +103,23 @@ public partial class Kubernetes uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/exec"; - var query = string.Empty; + var query = new StringBuilder(); foreach (var c in command) { - query = QueryHelpers.AddQueryString(query, "command", c); + Utilities.AddQueryParameter(query, "command", c); } - if (container != null) + if (!string.IsNullOrEmpty(container)) { - query = QueryHelpers.AddQueryString(query, "container", Uri.EscapeDataString(container)); + Utilities.AddQueryParameter(query, "container", container); } - query = QueryHelpers.AddQueryString(query, new Dictionary - { - {"stderr", stderr ? "1" : "0"}, - {"stdin", stdin ? "1" : "0"}, - {"stdout", stdout ? "1" : "0"}, - {"tty", tty ? "1" : "0"} - }).TrimStart('?'); - - uriBuilder.Query = query; + query.Append("&stderr=").Append(stderr ? '1' : '0'); // the query string is guaranteed not to be empty here because it has a 'command' param + query.Append("&stdin=").Append(stdin ? '1' : '0'); + query.Append("&stdout=").Append(stdout ? '1' : '0'); + query.Append("&tty=").Append(tty ? '1' : '0'); + uriBuilder.Query = query.ToString(1, query.Length-1); // UriBuilder.Query doesn't like leading '?' chars, so trim it return this.StreamConnectAsync(uriBuilder.Uri, _invocationId, webSocketSubProtol, customHeaders, cancellationToken); } @@ -171,14 +168,13 @@ public partial class Kubernetes uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward"; - var q = ""; + var q = new StringBuilder(); foreach (var port in ports) { - q = QueryHelpers.AddQueryString(q, "ports", $"{port}"); + if (q.Length != 0) q.Append('&'); + q.Append("ports=").Append(port.ToString(CultureInfo.InvariantCulture)); } - uriBuilder.Query = q.TrimStart('?'); - - + uriBuilder.Query = q.ToString(); return StreamConnectAsync(uriBuilder.Uri, _invocationId, webSocketSubProtocol, customHeaders, cancellationToken); } @@ -226,14 +222,13 @@ public partial class Kubernetes uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/attach"; - uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary - { - { "container", container}, - { "stderr", stderr ? "1": "0"}, - { "stdin", stdin ? "1": "0"}, - { "stdout", stdout ? "1": "0"}, - { "tty", tty ? "1": "0"} - }).TrimStart('?'); + var query = new StringBuilder(); + query.Append("?stderr=").Append(stderr ? '1' : '0'); + query.Append("&stdin=").Append(stdin ? '1' : '0'); + query.Append("&stdout=").Append(stdout ? '1' : '0'); + query.Append("&tty=").Append(tty ? '1' : '0'); + Utilities.AddQueryParameter(query, "container", container); + uriBuilder.Query = query.ToString(1, query.Length-1); // UriBuilder.Query doesn't like leading '?' chars, so trim it return StreamConnectAsync(uriBuilder.Uri, _invocationId, webSocketSubProtol, customHeaders, cancellationToken); } diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index bc9cdd058..0d403ff66 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -32,12 +32,11 @@ - - + diff --git a/src/KubernetesClient/Utilities.cs b/src/KubernetesClient/Utilities.cs new file mode 100644 index 000000000..1975ac381 --- /dev/null +++ b/src/KubernetesClient/Utilities.cs @@ -0,0 +1,17 @@ +using System; +using System.Text; + +namespace k8s +{ + internal static class Utilities + { + /// Given a that is building a query string, adds a parameter to it. + public static void AddQueryParameter(StringBuilder sb, string key, string value) + { + if (sb == null) throw new ArgumentNullException(nameof(sb)); + if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); + sb.Append(sb.Length != 0 ? '&' : '?').Append(Uri.EscapeDataString(key)).Append('='); + if (!string.IsNullOrEmpty(value)) sb.Append(Uri.EscapeDataString(value)); + } + } +} diff --git a/src/KubernetesClient/WatcherDelegatingHandler.cs b/src/KubernetesClient/WatcherDelegatingHandler.cs index 9fd753e56..cd553ac80 100644 --- a/src/KubernetesClient/WatcherDelegatingHandler.cs +++ b/src/KubernetesClient/WatcherDelegatingHandler.cs @@ -6,7 +6,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.WebUtilities; namespace k8s { @@ -21,11 +20,11 @@ protected override async Task SendAsync(HttpRequestMessage { var originResponse = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - if (originResponse.IsSuccessStatusCode) + if (originResponse.IsSuccessStatusCode && request.Method == HttpMethod.Get) // all watches are GETs, so we can ignore others { - var query = QueryHelpers.ParseQuery(request.RequestUri.Query); - - if (query.TryGetValue("watch", out var values) && values.Any(v => v == "true")) + string query = request.RequestUri.Query; + int index = query.IndexOf("watch=true"); + if (index > 0 && (query[index-1] == '&' || query[index-1] == '?')) { originResponse.Content = new LineSeparatedHttpContent(originResponse.Content, cancellationToken); } diff --git a/tests/KubernetesClient.Tests/Kubernetes.WebSockets.Tests.cs b/tests/KubernetesClient.Tests/Kubernetes.WebSockets.Tests.cs index 961ed1612..1c4bb8de0 100644 --- a/tests/KubernetesClient.Tests/Kubernetes.WebSockets.Tests.cs +++ b/tests/KubernetesClient.Tests/Kubernetes.WebSockets.Tests.cs @@ -58,7 +58,7 @@ public async Task WebSocketNamespacedPodExecAsync() }; Assert.Equal(mockWebSocketBuilder.PublicWebSocket, webSocket); // Did the method return the correct web socket? - Assert.Equal(new Uri("ws://localhost/api/v1/namespaces/mynamespace/pods/mypod/exec?command=%2Fbin%2Fbash&command=-c&command=echo%20Hello,%20World%0Aexit%200%0A&container=mycontainer&stderr=1&stdin=1&stdout=1&tty=1"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL? + Assert.Equal(new Uri("ws://localhost/api/v1/namespaces/mynamespace/pods/mypod/exec?command=%2Fbin%2Fbash&command=-c&command=echo%20Hello%2C%20World%0Aexit%200%0A&container=mycontainer&stderr=1&stdin=1&stdout=1&tty=1"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL? Assert.Empty(mockWebSocketBuilder.Certificates); // No certificates were used in this test Assert.Equal(expectedHeaders, mockWebSocketBuilder.RequestHeaders); // Did we use the expected headers } @@ -136,7 +136,7 @@ public async Task WebSocketNamespacedPodAttachAsync() }; Assert.Equal(mockWebSocketBuilder.PublicWebSocket, webSocket); // Did the method return the correct web socket? - Assert.Equal(new Uri("ws://localhost:80/api/v1/namespaces/mynamespace/pods/mypod/attach?container=my-container&stderr=1&stdin=1&stdout=1&tty=1"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL? + Assert.Equal(new Uri("ws://localhost:80/api/v1/namespaces/mynamespace/pods/mypod/attach?stderr=1&stdin=1&stdout=1&tty=1&container=my-container"), mockWebSocketBuilder.Uri); // Did we connect to the correct URL? Assert.Empty(mockWebSocketBuilder.Certificates); // No certificates were used in this test Assert.Equal(expectedHeaders, mockWebSocketBuilder.RequestHeaders); // Did we use the expected headers } diff --git a/tests/KubernetesClient.Tests/UtilityTests.cs b/tests/KubernetesClient.Tests/UtilityTests.cs new file mode 100644 index 000000000..02de7f998 --- /dev/null +++ b/tests/KubernetesClient.Tests/UtilityTests.cs @@ -0,0 +1,24 @@ +using System; +using System.Text; +using Xunit; + +namespace k8s.Tests +{ + public class UtilityTests + { + [Fact] + public void TestQueryStringUtilities() + { + var sb = new StringBuilder(); + Assert.Throws(() => Utilities.AddQueryParameter(null, "key", "value")); + Assert.Throws(() => Utilities.AddQueryParameter(sb, null, "value")); + Assert.Throws(() => Utilities.AddQueryParameter(sb, "", "value")); + + Utilities.AddQueryParameter(sb, "key", "value"); + Utilities.AddQueryParameter(sb, "key", "a=b"); + Utilities.AddQueryParameter(sb, "+key", null); + Utilities.AddQueryParameter(sb, "ekey", ""); + Assert.Equal("?key=value&key=a%3Db&%2Bkey=&ekey=", sb.ToString()); + } + } +}