Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
040600a
Initial plan
Copilot Jul 11, 2025
caabfcf
Implement uninstrumented peer visualization for parameters, connectio…
Copilot Jul 11, 2025
865eb7e
Support direct URL connection strings in peer resolution
Copilot Jul 11, 2025
59a9a59
Initial implementation of comprehensive connection string parser
Copilot Jul 11, 2025
4688044
Add comprehensive connection string parser with extensive test coverage
Copilot Jul 11, 2025
45deebf
Update src/Aspire.Dashboard/Model/ConnectionStringParser.cs
davidfowl Jul 12, 2025
4f93406
Fix failing ConnectionStringParser tests for comprehensive connection…
Copilot Jul 12, 2025
dd1439d
Refactor ConnectionStringParser with source-generated regexes and imp…
Copilot Jul 12, 2025
4e6f99e
Use ConnectionStringParser for Parameter resources and remove TryPars…
Copilot Jul 12, 2025
2de6751
Implement robust hostname validation using RFC-compliant logic
Copilot Jul 12, 2025
f7add63
Simplify hostname validation using URI parsing as suggested
Copilot Jul 12, 2025
b2c38be
Optimize ConnectionStringParser by using static readonly arrays and s…
Copilot Jul 12, 2025
e172cc4
Enhance GitHubModel resource initialization with connection string re…
davidfowl Jul 12, 2025
ba9ed81
Change ConnectionStringParser class from public to internal
Copilot Jul 12, 2025
19c344f
Refactor to eliminate nested transformer loops and extend change dete…
Copilot Jul 14, 2025
c2a3f37
Cache resource addresses on ResourceOutgoingPeerResolver to avoid rec…
Copilot Jul 14, 2025
3b2a877
Move cache from ResourceOutgoingPeerResolver to ResourceViewModel
Copilot Jul 14, 2025
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
Prev Previous commit
Next Next commit
Add comprehensive connection string parser with extensive test coverage
Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com>
  • Loading branch information
2 people authored and github-actions committed Jul 14, 2025
commit 46880446ade9964a7541707f659a709faaa07435
37 changes: 36 additions & 1 deletion src/Aspire.Dashboard/Model/ConnectionStringParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace Aspire.Dashboard.Model;
Expand Down Expand Up @@ -44,7 +45,7 @@ public static class ConnectionStringParser
["kafka"] = 9092
};

private static readonly string[] s_hostAliases = ["host", "server", "data source", "addr", "address", "endpoint"];
private static readonly string[] s_hostAliases = ["host", "server", "data source", "addr", "address", "endpoint", "contact points"];

private static readonly Regex s_hostPortRegex = new(@"(\[[^\]]+\]|[^,:;\s]+)[:|,](\d{1,5})", RegexOptions.Compiled);

Expand Down Expand Up @@ -83,6 +84,17 @@ public static bool TryDetectHostAndPort(
{
if (keyValuePairs.TryGetValue(hostAlias, out var token))
{
// First, check if the token is a complete URL
if (Uri.TryCreate(token, UriKind.Absolute, out var tokenUri) && !string.IsNullOrEmpty(tokenUri.Host))
{
host = TrimBrackets(tokenUri.Host);
port = tokenUri.Port != -1 ? tokenUri.Port : DefaultPortFromScheme(tokenUri.Scheme);
return true;
}

// Remove protocol prefixes like "tcp:", "udp:", etc. (but not from complete URLs)
token = RemoveProtocolPrefix(token);

if (token.Contains(',') || token.Contains(':'))
{
var (hostPart, portPart) = SplitOnLast(token);
Expand Down Expand Up @@ -129,6 +141,29 @@ public static bool TryDetectHostAndPort(

private static string TrimBrackets(string s) => s.Trim('[', ']');

private static string RemoveProtocolPrefix(string value)
{
// Remove common protocol prefixes like "tcp:", "udp:", "ssl:", etc.
if (string.IsNullOrEmpty(value))
{
return value;
}

var colonIndex = value.IndexOf(':');
if (colonIndex > 0 && colonIndex < value.Length - 1)
{
var prefix = value[..colonIndex].ToLowerInvariant();
// Only remove known protocol prefixes, not arbitrary single letters
var knownProtocols = new[] { "tcp", "udp", "ssl", "tls", "http", "https", "ftp", "ssh" };
if (knownProtocols.Contains(prefix))
{
return value[(colonIndex + 1)..];
}
}

return value;
}

private static int? DefaultPortFromScheme(string? scheme)
{
if (string.IsNullOrEmpty(scheme))
Expand Down
77 changes: 77 additions & 0 deletions tests/Aspire.Dashboard.Tests/ConnectionStringParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,83 @@ public class ConnectionStringParserTests
[InlineData("https://models.github.ai/inference", true, "models.github.ai", 443)]
[InlineData("Server=tcp:localhost,1433;Database=test", true, "localhost", 1433)]
[InlineData("Server=localhost;port=5432", true, "localhost", 5432)]
// SQL Server patterns
[InlineData("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;", true, "myServerAddress", null)]
[InlineData("Server=myServerAddress,1433;Database=myDataBase;Trusted_Connection=True;", true, "myServerAddress", 1433)]
[InlineData("Data Source=tcp:localhost,1433;Initial Catalog=TestDB;", true, "localhost", 1433)]
[InlineData("Data Source=.\\SQLEXPRESS;AttachDbFilename=|DataDirectory|mydbfile.mdf;Integrated Security=true;User Instance=true;", true, ".\\SQLEXPRESS", null)]
[InlineData("Server=(localdb)\\MSSQLLocalDB;Database=AspNetCore.StarterSite;Trusted_Connection=true;MultipleActiveResultSets=true", true, "(localdb)\\MSSQLLocalDB", null)]
// PostgreSQL patterns
[InlineData("Host=localhost;Database=mydb;Username=myuser;Password=mypass", true, "localhost", null)]
[InlineData("Host=localhost;Port=5432;Database=mydb;Username=myuser;Password=mypass", true, "localhost", 5432)]
[InlineData("postgresql://user:password@localhost:5432/dbname", true, "localhost", 5432)]
[InlineData("postgres://user:password@localhost/dbname", true, "localhost", 5432)]
// MySQL patterns
[InlineData("Server=localhost;Database=myDataBase;Uid=myUsername;Pwd=myPassword;", true, "localhost", null)]
[InlineData("Server=localhost;Port=3306;Database=myDataBase;Uid=myUsername;Pwd=myPassword;", true, "localhost", 3306)]
[InlineData("mysql://user:password@localhost:3306/database", true, "localhost", 3306)]
// MongoDB patterns
[InlineData("mongodb://localhost:27017", true, "localhost", 27017)]
[InlineData("mongodb://user:password@localhost:27017/database", true, "localhost", 27017)]
[InlineData("mongodb://localhost", true, "localhost", 27017)]
[InlineData("mongodb+srv://cluster0.example.mongodb.net/database", true, "cluster0.example.mongodb.net", null)]
// Redis patterns
[InlineData("localhost:6379", true, "localhost", 6379)]
[InlineData("redis://localhost:6379", true, "localhost", 6379)]
[InlineData("rediss://localhost:6380", true, "localhost", 6380)]
[InlineData("redis://user:password@localhost:6379/0", true, "localhost", 6379)]
[InlineData("Endpoint=localhost:6379;Password=mypassword", true, "localhost", 6379)]
// Oracle patterns
[InlineData("Data Source=localhost:1521/XE;User Id=hr;Password=password;", true, "localhost", null)] // Won't parse port from path syntax
// JDBC patterns (basic ones that should work - but many JDBC URLs are complex)
[InlineData("jdbc:postgresql://localhost:5432/database", true, "localhost", 5432)]
[InlineData("jdbc:mysql://localhost:3306/database", true, "localhost", 3306)]
[InlineData("jdbc:sqlserver://localhost:1433;databaseName=TestDB", true, "localhost", 1433)]
// Cloud provider patterns
[InlineData("https://myaccount.blob.core.windows.net/", true, "myaccount.blob.core.windows.net", 443)]
[InlineData("https://myvault.vault.azure.net:8080/", true, "myvault.vault.azure.net", 8080)]
[InlineData("Server=tcp:myserver.database.windows.net,1433;Database=mydatabase;", true, "myserver.database.windows.net", 1433)]
// Kafka patterns
[InlineData("localhost:9092,localhost:9093,localhost:9094", true, "localhost", 9092)]
[InlineData("broker-1:9092,broker-2:9092", true, "broker-1", 9092)]
// RabbitMQ patterns
[InlineData("amqp://localhost", true, "localhost", 5672)]
[InlineData("amqp://user:pass@localhost:5672/vhost", true, "localhost", 5672)]
[InlineData("amqps://localhost:5671", true, "localhost", 5671)]
[InlineData("Host=localhost;Port=5672;VirtualHost=/;Username=guest;Password=guest", true, "localhost", 5672)]
// Elasticsearch patterns
[InlineData("http://localhost:9200", true, "localhost", 9200)]
[InlineData("https://elastic:password@localhost:9200", true, "localhost", 9200)]
// InfluxDB patterns
[InlineData("http://localhost:8086", true, "localhost", 8086)]
[InlineData("https://localhost:8086", true, "localhost", 8086)]
// Cassandra patterns
[InlineData("Contact Points=localhost;Port=9042", true, "localhost", 9042)]
[InlineData("Contact Points=node1,node2,node3;Port=9042", false, "", null)] // Multiple contact points - too complex
// Neo4j patterns
[InlineData("bolt://localhost:7687", true, "localhost", 7687)]
[InlineData("neo4j://localhost:7687", true, "localhost", 7687)]
// Docker/container patterns
[InlineData("server.local", true, "server.local", null)]
[InlineData("my-service:5432", true, "my-service", 5432)]
[InlineData("my-namespace.my-service.svc.cluster.local:5432", true, "my-namespace.my-service.svc.cluster.local", 5432)]
// IPv6 patterns
[InlineData("Server=[::1],1433", true, "::1", 1433)]
[InlineData("Host=[2001:db8::1];Port=5432", true, "2001:db8::1", 5432)]
[InlineData("http://[2001:db8::1]:8080", true, "2001:db8::1", 8080)]
// Edge cases and invalid patterns
[InlineData("", false, "", null)]
[InlineData(" ", false, "", null)]
[InlineData("=", false, "", null)]
[InlineData("key=", false, "", null)]
[InlineData("=value", false, "", null)]
[InlineData("C:\\path\\to\\file.db", false, "", null)]
[InlineData("./relative/path/file.db", false, "", null)]
[InlineData("/absolute/path/file.db", false, "", null)]
[InlineData("just some random text", false, "", null)]
[InlineData("host=;port=5432", false, "", null)] // Empty host
[InlineData("server=localhost;port=abc", true, "localhost", null)] // Invalid port
[InlineData("server=localhost;port=99999", true, "localhost", null)] // Port out of range
public void TryDetectHostAndPort_VariousFormats_ReturnsExpectedResults(
string connectionString,
bool expectedResult,
Expand Down