Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Common/src/Common/Extensions/UriExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static bool TryGetUsernamePassword(this Uri uri, [NotNullWhen(true)] out

string userInfo = uri.GetComponents(UriComponents.UserInfo, UriFormat.UriEscaped);

string[] parts = userInfo.Split(':');
string[] parts = userInfo.Split(':', 2);

if (parts.Length == 2)
{
Expand Down
12 changes: 12 additions & 0 deletions src/Common/test/Common.Test/Extensions/UriExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,16 @@ public void DoNotMaskIfNoBasicAuthentication()

masked.Should().Be(expected);
}

[Fact]
public void TryGetUsernamePassword_AllowsColonInPassword()
{
var uri = new Uri("http://username:pass::[email protected]");

bool result = uri.TryGetUsernamePassword(out string? username, out string? password);

result.Should().BeTrue();
username.Should().Be("username");
password.Should().Be("pass::word");
}
}
16 changes: 13 additions & 3 deletions src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions
public bool FailFast { get; set; }

/// <summary>
/// Gets or sets the environment used when accessing configuration data. Default value: "Production".
/// Gets or sets a comma-separated list of environments used when accessing configuration data. Default value: "Production".
/// </summary>
[ConfigurationKeyName("Env")]
public string? Environment { get; set; } = "Production";
Expand Down Expand Up @@ -143,8 +143,18 @@ public bool ValidateCertificatesAlt
/// </summary>
public IDictionary<string, string> Headers { get; } = new Dictionary<string, string>();

internal IList<string> GetUris()
internal List<Uri> GetUris()
{
return !string.IsNullOrEmpty(Uri) ? Uri.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) : [];
return Uri == null ? [] : Uri.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(ParseSingleUri).ToList();
}

private static Uri ParseSingleUri(string uri)
{
if (!System.Uri.TryCreate(uri, UriKind.Absolute, out Uri? result))
{
throw new ConfigServerException("One or more Config Server URIs in configuration are invalid.");
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,24 +223,22 @@ public override void Load()
// Adds client settings (e.g. spring:cloud:config:uri, etc.) to the Data dictionary
AddConfigServerClientOptions();

string logUri = string.Join(',', ClientOptions.GetUris().Select(uri => new Uri(uri).ToMaskedString()));

if (ClientOptions is { Retry.Enabled: true, FailFast: true })
{
int attempts = 0;
int backOff = ClientOptions.Retry.InitialInterval;

do
{
_logger.LogDebug("Fetching configuration from server at: {Uri}", logUri);
_logger.LogDebug("Fetching configuration from server(s).");

try
{
return await DoLoadAsync(updateDictionary, cancellationToken);
}
catch (ConfigServerException exception)
{
_logger.LogWarning(exception, "Failed fetching configuration from server at: {Uri}.", logUri);
_logger.LogWarning(exception, "Failed fetching configuration from server(s).");
attempts++;

if (attempts < ClientOptions.Retry.MaxAttempts)
Expand All @@ -258,16 +256,16 @@ public override void Load()
while (true);
}

_logger.LogDebug("Fetching configuration from server at: {Uri}", logUri);
_logger.LogDebug("Fetching configuration from server(s).");
return await DoLoadAsync(updateDictionary, cancellationToken);
}

internal async Task<ConfigEnvironment?> DoLoadAsync(bool updateDictionary, CancellationToken cancellationToken)
{
Exception? error = null;

// Get arrays of Config Server uris to check
IList<string> uris = ClientOptions.GetUris();
// Get list of Config Server uris to check
List<Uri> uris = ClientOptions.GetUris();

try
{
Expand Down Expand Up @@ -543,7 +541,7 @@ private void AddConfigServerClientOptions(Dictionary<string, string?> data)
}
}

internal async Task<ConfigEnvironment?> RemoteLoadAsync(IEnumerable<string> requestUris, string? label, CancellationToken cancellationToken)
internal async Task<ConfigEnvironment?> RemoteLoadAsync(List<Uri> requestUris, string? label, CancellationToken cancellationToken)
{
_logger.LogTrace("Entered {Method}", nameof(RemoteLoadAsync));

Expand All @@ -552,11 +550,13 @@ private void AddConfigServerClientOptions(Dictionary<string, string?> data)

Exception? error = null;

foreach (string requestUri in requestUris)
foreach (Uri requestUri in requestUris)
{
// Make Config Server URI from settings
Uri uri = BuildConfigServerUri(requestUri, label);

_logger.LogDebug("Trying to connect to Config Server at {RequestUri}", uri.ToMaskedString());

// Get the request message
_logger.LogTrace("Building HTTP request message");
HttpRequestMessage request = await GetRequestMessageAsync(uri, cancellationToken);
Expand Down Expand Up @@ -622,23 +622,23 @@ private void AddConfigServerClientOptions(Dictionary<string, string?> data)
/// <returns>
/// The request URI for the Configuration Server.
/// </returns>
internal Uri BuildConfigServerUri(string serverUri, string? label)
internal Uri BuildConfigServerUri(Uri serverUri, string? label)
{
ArgumentException.ThrowIfNullOrWhiteSpace(serverUri);
ArgumentNullException.ThrowIfNull(serverUri);

var uriBuilder = new UriBuilder(new Uri(serverUri));
var uriBuilder = new UriBuilder(serverUri);

if (!string.IsNullOrEmpty(ClientOptions.Username))
{
uriBuilder.UserName = ClientOptions.Username;
uriBuilder.UserName = WebUtility.UrlEncode(ClientOptions.Username);
}

if (!string.IsNullOrEmpty(ClientOptions.Password))
{
uriBuilder.Password = ClientOptions.Password;
uriBuilder.Password = WebUtility.UrlEncode(ClientOptions.Password);
}

string pathSuffix = $"{ClientOptions.Name}/{ClientOptions.Environment}";
string pathSuffix = $"{WebUtility.UrlEncode(ClientOptions.Name)}/{WebUtility.UrlEncode(ClientOptions.Environment)}";

if (!string.IsNullOrWhiteSpace(label))
{
Expand All @@ -648,7 +648,7 @@ internal Uri BuildConfigServerUri(string serverUri, string? label)
label = label.Replace("/", "(_)", StringComparison.Ordinal);
}

pathSuffix = $"{pathSuffix}/{label.Trim()}";
pathSuffix = $"{pathSuffix}/{WebUtility.UrlEncode(label)}";
}

if (!uriBuilder.Path.EndsWith('/'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
},
"Env": {
"type": "string",
"description": "Gets or sets the environment used when accessing configuration data. Default value: \"Production\"."
"description": "Gets or sets a comma-separated list of environments used when accessing configuration data. Default value: \"Production\"."
},
"FailFast": {
"type": "boolean",
Expand Down
4 changes: 2 additions & 2 deletions src/Configuration/src/RandomValue/RandomValueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ private long GetLong()

private int GetNextIntInRange(string range)
{
string[] tokens = range.Split(',');
string[] tokens = range.Split(',', 2);
int.TryParse(tokens[0], CultureInfo.InvariantCulture, out int start);

if (tokens.Length == 1)
Expand All @@ -179,7 +179,7 @@ private int GetNextIntInRange(string range)

private long GetNextLongInRange(string range)
{
string[] tokens = range.Split(',');
string[] tokens = range.Split(',', 2);
long.TryParse(tokens[0], CultureInfo.InvariantCulture, out long start);

if (tokens.Length == 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public void AddConfigServer_WithLoggerFactorySucceeds()

IList<string> logMessages = loggerProvider.GetAll();

logMessages.Should().Contain(
"DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server at: http://localhost:8888/");
logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s).");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ public void AddConfigServer_WithLoggerFactorySucceeds()

IList<string> logMessages = loggerProvider.GetAll();

logMessages.Should().Contain(
"DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server at: http://localhost:8888/");
logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s).");
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,6 @@ namespace Steeltoe.Configuration.ConfigServer.Test;

public sealed partial class ConfigServerConfigurationProviderTest
{
[Fact]
public async Task RemoteLoadAsync_InvalidUri()
{
var options = new ConfigServerClientOptions();
using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);

// ReSharper disable once AccessToDisposedClosure
Func<Task> action = async () => await provider.RemoteLoadAsync([@"foobar\foobar\"], null, TestContext.Current.CancellationToken);

await action.Should().ThrowExactlyAsync<UriFormatException>();
}

[Fact]
public async Task RemoteLoadAsync_HostTimesOut()
{
Expand All @@ -35,9 +23,10 @@ public async Task RemoteLoadAsync_HostTimesOut()

var httpClientHandler = new SlowHttpClientHandler(1.Seconds(), new HttpResponseMessage());
using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);
List<Uri> requestUris = [new("http://localhost:9999/app/profile")];

// ReSharper disable once AccessToDisposedClosure
Func<Task> action = async () => await provider.RemoteLoadAsync(["http://localhost:9999/app/profile"], null, TestContext.Current.CancellationToken);
Func<Task> action = async () => await provider.RemoteLoadAsync(requestUris, null, TestContext.Current.CancellationToken);

(await action.Should().ThrowExactlyAsync<TaskCanceledException>()).WithInnerExceptionExactly<TimeoutException>();
}
Expand Down Expand Up @@ -506,6 +495,32 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti
startup.RequestCount.Should().Be(1);
}

[Fact]
public async Task Load_UriInvalid_FailFastEnabled()
{
using var startup = new TestConfigServerStartup();
startup.ReturnStatus = [500];

await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build();
startup.Configure(app);
await app.StartAsync(TestContext.Current.CancellationToken);

using TestServer server = app.GetTestServer();
server.BaseAddress = new Uri("http://localhost:8888");

ConfigServerClientOptions options = GetCommonOptions();
options.Uri = "http://username:p@ssword@localhost:8888";
options.FailFast = true;

using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler());
using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance);

// ReSharper disable once AccessToDisposedClosure
Func<Task> action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken);

await action.Should().ThrowExactlyAsync<ConfigServerException>().WithMessage("One or more Config Server URIs in configuration are invalid.");
}

[Fact]
public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ public void GetConfigServerUri_NoLabel()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri!, null).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), null).ToString();

path.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}");
uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}");
}

[Fact]
Expand All @@ -95,9 +95,9 @@ public void GetConfigServerUri_WithLabel()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri!, options.Label).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();

path.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}");
uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}");
}

[Fact]
Expand All @@ -111,9 +111,9 @@ public void GetConfigServerUri_WithLabelContainingSlash()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri!, options.Label).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();

path.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version");
uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version");
}

[Fact]
Expand All @@ -127,9 +127,9 @@ public void GetConfigServerUri_WithExtraPathInfo()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri, null).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();

path.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}");
uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}");
}

[Fact]
Expand All @@ -143,9 +143,9 @@ public void GetConfigServerUri_WithExtraPathInfo_NoEndingSlash()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri, null).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();

path.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}");
uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}");
}

[Fact]
Expand All @@ -159,9 +159,9 @@ public void GetConfigServerUri_NoEndingSlash()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri, null).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();

path.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}");
uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}");
}

[Fact]
Expand All @@ -175,8 +175,43 @@ public void GetConfigServerUri_WithEndingSlash()
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string path = provider.BuildConfigServerUri(options.Uri, null).ToString();
string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString();

path.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}");
uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}");
}

[Fact]
public void GetConfigServerUri_MultipleEnvironments_EncodesComma()
{
var options = new ConfigServerClientOptions
{
Name = "myName",
Environment = "one,two",
Label = "demo"
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();

uri.Should().Be("http://localhost:8888/myName/one%2Ctwo/demo");
}

[Fact]
public void GetConfigServerUri_EncodesSpecialCharacters()
{
var options = new ConfigServerClientOptions
{
Name = "my$<>:,\"'Name",
Environment = "@/&?:\"'",
Label = "^&$@#*<>+",
Username = "a\":?'*,b/\\&",
Password = "#&:$<>'/so,\"me"
};

using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance);
string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString();

uri.Should().Be(
"http://a%22%3A%3F%27*%2Cb%2F%5C%26:%23%26%3A%24%3C%3E%27%2Fso%2C%22me@localhost:8888/my%24<>%3A%2C\"%27Name/%40%2F%26%3F%3A\"%27/^%26%24%40%23*<>%2B");
}
}
Loading
Loading