diff --git a/src/csharp/Grpc.Auth/GoogleCredential.cs b/src/csharp/Grpc.Auth/GoogleCredential.cs deleted file mode 100644 index 9936cf583ca..00000000000 --- a/src/csharp/Grpc.Auth/GoogleCredential.cs +++ /dev/null @@ -1,125 +0,0 @@ -#region Copyright notice and license - -// Copyright 2015, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; - -using Google.Apis.Auth.OAuth2; -using Google.Apis.Auth.OAuth2.Responses; -using Newtonsoft.Json.Linq; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Security; - -namespace Grpc.Auth -{ - // TODO(jtattermusch): Remove this class once possible. - /// - /// A temporary placeholder for Google credential from - /// Google Auth library for .NET. It emulates the usage pattern - /// for Usable auth. - /// - public class GoogleCredential - { - private const string GoogleApplicationCredentialsEnvName = "GOOGLE_APPLICATION_CREDENTIALS"; - private const string ClientEmailFieldName = "client_email"; - private const string PrivateKeyFieldName = "private_key"; - - private ServiceCredential credential; - - private GoogleCredential(ServiceCredential credential) - { - this.credential = credential; - } - - public static GoogleCredential GetApplicationDefault() - { - return new GoogleCredential(null); - } - - public bool IsCreateScopedRequired - { - get - { - return true; - } - } - - public GoogleCredential CreateScoped(IEnumerable scopes) - { - var credsPath = Environment.GetEnvironmentVariable(GoogleApplicationCredentialsEnvName); - if (credsPath == null) - { - // Default to ComputeCredentials if path to JSON key is not set. - // ComputeCredential is not scoped actually, but for our use case it's - // fine to treat is as such. - return new GoogleCredential(new ComputeCredential(new ComputeCredential.Initializer())); - } - - JObject jsonCredentialParameters = JObject.Parse(File.ReadAllText(credsPath)); - string clientEmail = jsonCredentialParameters.GetValue(ClientEmailFieldName).Value(); - string privateKeyString = jsonCredentialParameters.GetValue(PrivateKeyFieldName).Value(); - - var serviceCredential = new ServiceAccountCredential( - new ServiceAccountCredential.Initializer(clientEmail) - { - Scopes = scopes, - }.FromPrivateKey(privateKeyString)); - return new GoogleCredential(serviceCredential); - } - - public Task RequestAccessTokenAsync(CancellationToken taskCancellationToken) - { - return credential.RequestAccessTokenAsync(taskCancellationToken); - } - - public TokenResponse Token - { - get - { - return credential.Token; - } - } - - internal ServiceCredential InternalCredential - { - get - { - return credential; - } - } - } -} diff --git a/src/csharp/Grpc.Auth/Grpc.Auth.csproj b/src/csharp/Grpc.Auth/Grpc.Auth.csproj index 8e5036832d5..930a34b0c33 100644 --- a/src/csharp/Grpc.Auth/Grpc.Auth.csproj +++ b/src/csharp/Grpc.Auth/Grpc.Auth.csproj @@ -11,7 +11,7 @@ Grpc.Auth v4.5 bin\$(Configuration)\Grpc.Auth.Xml - 9b408026 + 4f8487a9 true @@ -41,28 +41,32 @@ C:\keys\Grpc.snk - + + False ..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll - + False - ..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.dll + ..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.dll - + False - ..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.PlatformServices.dll + ..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.PlatformServices.dll - + False - ..\packages\Google.Apis.Core.1.9.2\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll + ..\packages\Google.Apis.Core.1.9.3\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll - + + False ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - + + False ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - + + False ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll @@ -87,7 +91,6 @@ Version.cs - diff --git a/src/csharp/Grpc.Auth/OAuth2Interceptors.cs b/src/csharp/Grpc.Auth/OAuth2Interceptors.cs index cc9d2c175ff..d628a83246d 100644 --- a/src/csharp/Grpc.Auth/OAuth2Interceptors.cs +++ b/src/csharp/Grpc.Auth/OAuth2Interceptors.cs @@ -54,7 +54,7 @@ namespace Grpc.Auth /// public static MetadataInterceptorDelegate FromCredential(GoogleCredential googleCredential) { - var interceptor = new OAuth2Interceptor(googleCredential.InternalCredential, SystemClock.Default); + var interceptor = new OAuth2Interceptor(googleCredential, SystemClock.Default); return new MetadataInterceptorDelegate(interceptor.InterceptHeaders); } @@ -66,7 +66,7 @@ namespace Grpc.Auth public static MetadataInterceptorDelegate FromAccessToken(string oauth2Token) { Preconditions.CheckNotNull(oauth2Token); - return new MetadataInterceptorDelegate((metadata) => + return new MetadataInterceptorDelegate((authUri, metadata) => { metadata.Add(OAuth2Interceptor.CreateBearerTokenHeader(oauth2Token)); }); @@ -80,10 +80,10 @@ namespace Grpc.Auth private const string AuthorizationHeader = "Authorization"; private const string Schema = "Bearer"; - private ServiceCredential credential; + private ITokenAccess credential; private IClock clock; - public OAuth2Interceptor(ServiceCredential credential, IClock clock) + public OAuth2Interceptor(ITokenAccess credential, IClock clock) { this.credential = credential; this.clock = clock; @@ -94,23 +94,15 @@ namespace Grpc.Auth /// /// /// - public string GetAccessToken(CancellationToken cancellationToken) + public string GetAccessToken(string authUri, CancellationToken cancellationToken) { - if (credential.Token == null || credential.Token.IsExpired(clock)) - { - // TODO(jtattermusch): Parallel requests will spawn multiple requests to refresh the token once the token expires. - // TODO(jtattermusch): Rethink synchronous wait to obtain the result. - if (!credential.RequestAccessTokenAsync(cancellationToken).Result) - { - throw new InvalidOperationException("The access token has expired but we can't refresh it"); - } - } - return credential.Token.AccessToken; + // TODO(jtattermusch): Rethink synchronous wait to obtain the result. + return credential.GetAccessTokenForRequestAsync(authUri, cancellationToken: cancellationToken).GetAwaiter().GetResult(); } - public void InterceptHeaders(Metadata metadata) + public void InterceptHeaders(string authUri, Metadata metadata) { - var accessToken = GetAccessToken(CancellationToken.None); + var accessToken = GetAccessToken(authUri, CancellationToken.None); metadata.Add(CreateBearerTokenHeader(accessToken)); } diff --git a/src/csharp/Grpc.Auth/app.config b/src/csharp/Grpc.Auth/app.config index 0a82bb4f16c..84d7534d650 100644 --- a/src/csharp/Grpc.Auth/app.config +++ b/src/csharp/Grpc.Auth/app.config @@ -10,6 +10,10 @@ + + + + \ No newline at end of file diff --git a/src/csharp/Grpc.Auth/packages.config b/src/csharp/Grpc.Auth/packages.config index 29be953bf3e..7a02c95db91 100644 --- a/src/csharp/Grpc.Auth/packages.config +++ b/src/csharp/Grpc.Auth/packages.config @@ -1,8 +1,8 @@  - - + + diff --git a/src/csharp/Grpc.Core.Tests/ChannelTest.cs b/src/csharp/Grpc.Core.Tests/ChannelTest.cs index 90f6e570c4c..27875729240 100644 --- a/src/csharp/Grpc.Core.Tests/ChannelTest.cs +++ b/src/csharp/Grpc.Core.Tests/ChannelTest.cs @@ -72,11 +72,11 @@ namespace Grpc.Core.Tests } [Test] - public void Target() + public void ResolvedTarget() { using (var channel = new Channel("127.0.0.1", Credentials.Insecure)) { - Assert.IsTrue(channel.Target.Contains("127.0.0.1")); + Assert.IsTrue(channel.ResolvedTarget.Contains("127.0.0.1")); } } diff --git a/src/csharp/Grpc.Core/Channel.cs b/src/csharp/Grpc.Core/Channel.cs index f8cbe08466c..64c6adf2bfc 100644 --- a/src/csharp/Grpc.Core/Channel.cs +++ b/src/csharp/Grpc.Core/Channel.cs @@ -49,6 +49,7 @@ namespace Grpc.Core { static readonly ILogger Logger = GrpcEnvironment.Logger.ForType(); + readonly string target; readonly GrpcEnvironment environment; readonly ChannelSafeHandle handle; readonly List options; @@ -58,12 +59,12 @@ namespace Grpc.Core /// Creates a channel that connects to a specific host. /// Port will default to 80 for an unsecure channel and to 443 for a secure channel. /// - /// The name or IP address of the host. + /// Target of the channel. /// Credentials to secure the channel. /// Channel options. - public Channel(string host, Credentials credentials, IEnumerable options = null) + public Channel(string target, Credentials credentials, IEnumerable options = null) { - Preconditions.CheckNotNull(host, "host"); + this.target = Preconditions.CheckNotNull(target, "target"); this.environment = GrpcEnvironment.GetInstance(); this.options = options != null ? new List(options) : new List(); @@ -73,11 +74,11 @@ namespace Grpc.Core { if (nativeCredentials != null) { - this.handle = ChannelSafeHandle.CreateSecure(nativeCredentials, host, nativeChannelArgs); + this.handle = ChannelSafeHandle.CreateSecure(nativeCredentials, target, nativeChannelArgs); } else { - this.handle = ChannelSafeHandle.CreateInsecure(host, nativeChannelArgs); + this.handle = ChannelSafeHandle.CreateInsecure(target, nativeChannelArgs); } } } @@ -131,8 +132,8 @@ namespace Grpc.Core return tcs.Task; } - /// Address of the remote endpoint in URI format. - public string Target + /// Resolved address of the remote endpoint in URI format. + public string ResolvedTarget { get { @@ -140,6 +141,15 @@ namespace Grpc.Core } } + /// The original target used to create the channel. + public string Target + { + get + { + return this.target; + } + } + /// /// Allows explicitly requesting channel to connect without starting an RPC. /// Returned task completes once state Ready was seen. If the deadline is reached, diff --git a/src/csharp/Grpc.Core/ClientBase.cs b/src/csharp/Grpc.Core/ClientBase.cs index 48fc7ed34a5..f46184406c5 100644 --- a/src/csharp/Grpc.Core/ClientBase.cs +++ b/src/csharp/Grpc.Core/ClientBase.cs @@ -35,21 +35,26 @@ using System; using System.Collections.Generic; using Grpc.Core.Internal; +using System.Text.RegularExpressions; namespace Grpc.Core { - public delegate void MetadataInterceptorDelegate(Metadata metadata); + public delegate void MetadataInterceptorDelegate(string authUri, Metadata metadata); /// /// Base class for client-side stubs. /// public abstract class ClientBase { + static readonly Regex TrailingPortPattern = new Regex(":[0-9]+/?$"); readonly Channel channel; + readonly string authUriBase; public ClientBase(Channel channel) { this.channel = channel; + // TODO(jtattermush): we shouldn't need to hand-curate the channel.Target contents. + this.authUriBase = "https://" + TrailingPortPattern.Replace(channel.Target, "") + "/"; } /// @@ -99,8 +104,8 @@ namespace Grpc.Core { options = options.WithHeaders(new Metadata()); } - interceptor(options.Headers); - options.Headers.Freeze(); + var authUri = authUriBase + method.ServiceName; + interceptor(authUri, options.Headers); } return new CallInvocationDetails(channel, method, Host, options); } diff --git a/src/csharp/Grpc.IntegrationTesting.Client/app.config b/src/csharp/Grpc.IntegrationTesting.Client/app.config index 0a82bb4f16c..84d7534d650 100644 --- a/src/csharp/Grpc.IntegrationTesting.Client/app.config +++ b/src/csharp/Grpc.IntegrationTesting.Client/app.config @@ -10,6 +10,10 @@ + + + + \ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting.Server/app.config b/src/csharp/Grpc.IntegrationTesting.Server/app.config index 0a82bb4f16c..84d7534d650 100644 --- a/src/csharp/Grpc.IntegrationTesting.Server/app.config +++ b/src/csharp/Grpc.IntegrationTesting.Server/app.config @@ -10,6 +10,10 @@ + + + + \ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj index 06a75a3351e..2020a76d396 100644 --- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj +++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj @@ -8,7 +8,7 @@ Grpc.IntegrationTesting Grpc.IntegrationTesting v4.5 - 041c163e + 6566287f true @@ -38,20 +38,33 @@ C:\keys\Grpc.snk - + + False ..\packages\BouncyCastle.1.7.0\lib\Net40-Client\BouncyCastle.Crypto.dll - + + False + ..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.dll + + + False + ..\packages\Google.Apis.Auth.1.9.3\lib\net40\Google.Apis.Auth.PlatformServices.dll + + + False + ..\packages\Google.Apis.Core.1.9.3\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll + + False - ..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.dll + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - + False - ..\packages\Google.Apis.Auth.1.9.2\lib\net40\Google.Apis.Auth.PlatformServices.dll + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - + False - ..\packages\Google.Apis.Core.1.9.2\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll + ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll False @@ -78,15 +91,6 @@ ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll - diff --git a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs index c918f60127a..385ca920862 100644 --- a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs +++ b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs @@ -43,6 +43,7 @@ using Grpc.Auth; using Grpc.Core; using Grpc.Core.Utils; using NUnit.Framework; +using Google.Apis.Auth.OAuth2; namespace Grpc.IntegrationTesting { @@ -97,10 +98,10 @@ namespace Grpc.IntegrationTesting } var interopClient = new InteropClient(options); - interopClient.Run(); + interopClient.Run().Wait(); } - private void Run() + private async Task Run() { Credentials credentials = null; if (options.useTls) @@ -120,17 +121,7 @@ namespace Grpc.IntegrationTesting using (Channel channel = new Channel(options.serverHost, options.serverPort.Value, credentials, channelOptions)) { TestService.TestServiceClient client = new TestService.TestServiceClient(channel); - if (options.testCase == "service_account_creds" || options.testCase == "compute_engine_creds") - { - var credential = GoogleCredential.GetApplicationDefault(); - if (credential.IsCreateScopedRequired) - { - credential = credential.CreateScoped(new[] { AuthScope }); - } - client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); - } - - RunTestCaseAsync(options.testCase, client).Wait(); + await RunTestCaseAsync(options.testCase, client); } GrpcEnvironment.Shutdown(); } @@ -158,16 +149,19 @@ namespace Grpc.IntegrationTesting await RunEmptyStreamAsync(client); break; case "service_account_creds": - RunServiceAccountCreds(client); + await RunServiceAccountCredsAsync(client); break; case "compute_engine_creds": - RunComputeEngineCreds(client); + await RunComputeEngineCredsAsync(client); + break; + case "jwt_token_creds": + await RunJwtTokenCredsAsync(client); break; case "oauth2_auth_token": - RunOAuth2AuthToken(client); + await RunOAuth2AuthTokenAsync(client); break; case "per_rpc_creds": - RunPerRpcCreds(client); + await RunPerRpcCredsAsync(client); break; case "cancel_after_begin": await RunCancelAfterBeginAsync(client); @@ -309,9 +303,13 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunServiceAccountCreds(TestService.ITestServiceClient client) + public static async Task RunServiceAccountCredsAsync(TestService.TestServiceClient client) { Console.WriteLine("running service_account_creds"); + var credential = await GoogleCredential.GetApplicationDefaultAsync(); + credential = credential.CreateScoped(new[] { AuthScope }); + client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); + var request = SimpleRequest.CreateBuilder() .SetResponseType(PayloadType.COMPRESSABLE) .SetResponseSize(314159) @@ -329,9 +327,13 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunComputeEngineCreds(TestService.ITestServiceClient client) + public static async Task RunComputeEngineCredsAsync(TestService.TestServiceClient client) { Console.WriteLine("running compute_engine_creds"); + var credential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsFalse(credential.IsCreateScopedRequired); + client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); + var request = SimpleRequest.CreateBuilder() .SetResponseType(PayloadType.COMPRESSABLE) .SetResponseSize(314159) @@ -349,12 +351,35 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunOAuth2AuthToken(TestService.TestServiceClient client) + public static async Task RunJwtTokenCredsAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running jwt_token_creds"); + var credential = await GoogleCredential.GetApplicationDefaultAsync(); + // check this a credential with scope support, but don't add the scope. + Assert.IsTrue(credential.IsCreateScopedRequired); + client.HeaderInterceptor = OAuth2Interceptors.FromCredential(credential); + + var request = SimpleRequest.CreateBuilder() + .SetResponseType(PayloadType.COMPRESSABLE) + .SetResponseSize(314159) + .SetPayload(CreateZerosPayload(271828)) + .SetFillUsername(true) + .SetFillOauthScope(true) + .Build(); + + var response = client.UnaryCall(request); + + Assert.AreEqual(PayloadType.COMPRESSABLE, response.Payload.Type); + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.AreEqual(ServiceAccountUser, response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunOAuth2AuthTokenAsync(TestService.TestServiceClient client) { Console.WriteLine("running oauth2_auth_token"); - var credential = GoogleCredential.GetApplicationDefault().CreateScoped(new[] { AuthScope }); - Assert.IsTrue(credential.RequestAccessTokenAsync(CancellationToken.None).Result); - string oauth2Token = credential.Token.AccessToken; + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { AuthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); client.HeaderInterceptor = OAuth2Interceptors.FromAccessToken(oauth2Token); @@ -370,13 +395,12 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } - public static void RunPerRpcCreds(TestService.TestServiceClient client) + public static async Task RunPerRpcCredsAsync(TestService.TestServiceClient client) { Console.WriteLine("running per_rpc_creds"); - var credential = GoogleCredential.GetApplicationDefault().CreateScoped(new[] { AuthScope }); - Assert.IsTrue(credential.RequestAccessTokenAsync(CancellationToken.None).Result); - string oauth2Token = credential.Token.AccessToken; + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { AuthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); var headerInterceptor = OAuth2Interceptors.FromAccessToken(oauth2Token); var request = SimpleRequest.CreateBuilder() @@ -385,7 +409,7 @@ namespace Grpc.IntegrationTesting .Build(); var headers = new Metadata(); - headerInterceptor(headers); + headerInterceptor("", headers); var response = client.UnaryCall(request, headers: headers); Assert.AreEqual(AuthScopeResponse, response.OauthScope); diff --git a/src/csharp/Grpc.IntegrationTesting/app.config b/src/csharp/Grpc.IntegrationTesting/app.config index 0a82bb4f16c..84d7534d650 100644 --- a/src/csharp/Grpc.IntegrationTesting/app.config +++ b/src/csharp/Grpc.IntegrationTesting/app.config @@ -10,6 +10,10 @@ + + + + \ No newline at end of file diff --git a/src/csharp/Grpc.IntegrationTesting/packages.config b/src/csharp/Grpc.IntegrationTesting/packages.config index 7d1f84f3031..0867b091b92 100644 --- a/src/csharp/Grpc.IntegrationTesting/packages.config +++ b/src/csharp/Grpc.IntegrationTesting/packages.config @@ -1,8 +1,8 @@  - - + +