- 
                Notifications
    You must be signed in to change notification settings 
- Fork 715
Support uninstrumented peer visualization for parameters, and resources with connection strings and github models #10340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
189fd32
              df9960b
              d1fb28f
              138b2a7
              5f58e3f
              40c8305
              748f053
              7d232fc
              2c7129f
              01ae0cb
              40f8cb8
              fe4c50f
              7334208
              0792ea7
              67540a7
              d0c1afb
              b7c2862
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|  | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Globalization; | ||
| using System.Linq; | ||
| Check failure on line 6 in src/Aspire.Dashboard/Model/ConnectionStringParser.cs 
     | ||
| using System.Text.RegularExpressions; | ||
|  | ||
| namespace Aspire.Dashboard.Model; | ||
|  | ||
| /// <summary> | ||
| /// Provides utilities for parsing connection strings to extract host and port information. | ||
| /// </summary> | ||
| public static class ConnectionStringParser | ||
|         
                  davidfowl marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| { | ||
| private static readonly Dictionary<string, int> s_schemeDefaultPorts = new(StringComparer.OrdinalIgnoreCase) | ||
| { | ||
| ["http"] = 80, | ||
| ["https"] = 443, | ||
| ["ftp"] = 21, | ||
| ["ftps"] = 990, | ||
| ["ssh"] = 22, | ||
| ["telnet"] = 23, | ||
| ["smtp"] = 25, | ||
| ["dns"] = 53, | ||
| ["dhcp"] = 67, | ||
| ["tftp"] = 69, | ||
| ["pop3"] = 110, | ||
| ["ntp"] = 123, | ||
| ["imap"] = 143, | ||
| ["snmp"] = 161, | ||
| ["ldap"] = 389, | ||
| ["smtps"] = 465, | ||
| ["ldaps"] = 636, | ||
| ["imaps"] = 993, | ||
| ["pop3s"] = 995, | ||
| ["mssql"] = 1433, | ||
| ["mysql"] = 3306, | ||
| ["postgresql"] = 5432, | ||
| ["postgres"] = 5432, | ||
| ["redis"] = 6379, | ||
| ["mongodb"] = 27017, | ||
| ["amqp"] = 5672, | ||
| ["amqps"] = 5671, | ||
| ["kafka"] = 9092 | ||
| }; | ||
|  | ||
| 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); | ||
|  | ||
| /// <summary> | ||
| /// Attempts to extract a host and optional port from an arbitrary connection string. | ||
| /// Returns <c>true</c> if a host could be identified; otherwise <c>false</c>. | ||
| /// </summary> | ||
| /// <param name="connectionString">The connection string to parse.</param> | ||
| /// <param name="host">When this method returns <c>true</c>, contains the host part with surrounding brackets removed; otherwise, an empty string.</param> | ||
| /// <param name="port">When this method returns <c>true</c>, contains the explicit port, scheme-derived default, or <c>null</c> when unavailable; otherwise, <c>null</c>.</param> | ||
| /// <returns><c>true</c> if a host was found; otherwise, <c>false</c>.</returns> | ||
| public static bool TryDetectHostAndPort( | ||
| string connectionString, | ||
| [NotNullWhen(true)] out string? host, | ||
| out int? port) | ||
| { | ||
| host = null; | ||
| port = null; | ||
|  | ||
| if (string.IsNullOrWhiteSpace(connectionString)) | ||
| { | ||
| return false; | ||
| } | ||
|  | ||
| // 1. URI parse | ||
| if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri) && !string.IsNullOrEmpty(uri.Host)) | ||
| { | ||
| host = TrimBrackets(uri.Host); | ||
| port = uri.Port != -1 ? uri.Port : DefaultPortFromScheme(uri.Scheme); | ||
| return true; | ||
| } | ||
|  | ||
| // 2. Key-value scan | ||
| var keyValuePairs = SplitIntoDictionary(connectionString); | ||
| foreach (var hostAlias in s_hostAliases) | ||
| { | ||
| 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); | ||
| if (!string.IsNullOrEmpty(hostPart)) | ||
| { | ||
| host = TrimBrackets(hostPart); | ||
| port = ParseIntSafe(portPart) ?? PortFromKV(keyValuePairs); | ||
| return true; | ||
| } | ||
| } | ||
| else if (!string.IsNullOrEmpty(token)) | ||
| { | ||
| host = TrimBrackets(token); | ||
| port = PortFromKV(keyValuePairs); | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|  | ||
| // 3. Regex heuristic for host:port or host,port patterns | ||
| var match = s_hostPortRegex.Match(connectionString); | ||
| if (match.Success) | ||
| { | ||
| var hostPart = match.Groups[1].Value; | ||
| var portPart = match.Groups[2].Value; | ||
| if (!string.IsNullOrEmpty(hostPart)) | ||
| { | ||
| host = TrimBrackets(hostPart); | ||
| port = ParseIntSafe(portPart); | ||
| return true; | ||
| } | ||
| } | ||
|  | ||
| // 4. Looks like single host token (no '=' etc.) | ||
| if (LooksLikeHost(connectionString)) | ||
| { | ||
| host = TrimBrackets(connectionString); | ||
| port = null; | ||
| return true; | ||
| } | ||
|  | ||
| return false; | ||
| } | ||
|  | ||
| 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" }; | ||
|         
                  davidfowl marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| if (knownProtocols.Contains(prefix)) | ||
| { | ||
| return value[(colonIndex + 1)..]; | ||
| } | ||
| } | ||
|  | ||
| return value; | ||
| } | ||
|  | ||
| private static int? DefaultPortFromScheme(string? scheme) | ||
| { | ||
| if (string.IsNullOrEmpty(scheme)) | ||
| { | ||
| return null; | ||
| } | ||
|  | ||
| return s_schemeDefaultPorts.TryGetValue(scheme, out var port) ? port : null; | ||
| } | ||
|  | ||
| private static int? PortFromKV(Dictionary<string, string> keyValuePairs) | ||
| { | ||
| return keyValuePairs.TryGetValue("port", out var portValue) ? ParseIntSafe(portValue) : null; | ||
| } | ||
|  | ||
| private static int? ParseIntSafe(string? s) | ||
| { | ||
| if (string.IsNullOrEmpty(s)) | ||
| { | ||
| return null; | ||
| } | ||
|  | ||
| if (int.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var value) && | ||
| value >= 0 && value <= 65535) | ||
| { | ||
| return value; | ||
| } | ||
|  | ||
| return null; | ||
| } | ||
|  | ||
| private static Dictionary<string, string> SplitIntoDictionary(string connectionString) | ||
| { | ||
| var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | ||
|  | ||
| // Split by semicolon first, then by whitespace if no semicolons found | ||
| var parts = connectionString.Contains(';') | ||
| ? connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries) | ||
| : connectionString.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries); | ||
|  | ||
| foreach (var part in parts) | ||
| { | ||
| var trimmedPart = part.Trim(); | ||
| var equalIndex = trimmedPart.IndexOf('='); | ||
| if (equalIndex > 0 && equalIndex < trimmedPart.Length - 1) | ||
| { | ||
| var key = trimmedPart[..equalIndex].Trim(); | ||
| var value = trimmedPart[(equalIndex + 1)..].Trim(); | ||
| if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) | ||
| { | ||
| result[key] = value; | ||
| } | ||
| } | ||
| } | ||
|  | ||
| return result; | ||
| } | ||
|  | ||
| private static (string host, string port) SplitOnLast(string token) | ||
| { | ||
| // Split on the last occurrence of ':' or ',' | ||
| var lastColonIndex = token.LastIndexOf(':'); | ||
| var lastCommaIndex = token.LastIndexOf(','); | ||
| var splitIndex = Math.Max(lastColonIndex, lastCommaIndex); | ||
|  | ||
| if (splitIndex > 0 && splitIndex < token.Length - 1) | ||
| { | ||
| return (token[..splitIndex].Trim(), token[(splitIndex + 1)..].Trim()); | ||
| } | ||
|  | ||
| return (token, string.Empty); | ||
| } | ||
|  | ||
| private static bool LooksLikeHost(string connectionString) | ||
|         
                  davidfowl marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| { | ||
| // Simple heuristic: if it doesn't contain '=' and looks like a hostname or IP | ||
| if (connectionString.Contains('=')) | ||
| { | ||
| return false; | ||
| } | ||
|  | ||
| // Remove common file path indicators | ||
| if (connectionString.StartsWith('/') || connectionString.StartsWith('\\') || | ||
| (connectionString.Length > 2 && connectionString[1] == ':' && char.IsLetter(connectionString[0]))) | ||
| { | ||
| return false; | ||
| } | ||
|  | ||
| // Should contain dots (for domains) or be a simple name, and not contain spaces | ||
| var trimmed = connectionString.Trim(); | ||
| return !string.IsNullOrEmpty(trimmed) && | ||
| !trimmed.Contains(' ') && | ||
| (trimmed.Contains('.') || !trimmed.Contains('/')); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.