using System; using System.IO; using System.Text.RegularExpressions; namespace BizTalkCheckmkPulse { /// /// Reduziert adapter-spezifische BizTalk-Adressen auf einen geheimnisfreien Host/Port-Test. /// internal static class EndpointAddressParser { private static readonly Regex HostPortPattern = new Regex( @"^(?\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?\d{1,5})(?:[/\\].*)?$", RegexOptions.CultureInvariant); /// /// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag. /// public static bool TryCreate( EndpointCandidate candidate, out EndpointCatalogEntry entry, out string reason) { var resolution = Analyze(candidate); entry = resolution.Entry; reason = resolution.Reason ?? string.Empty; return resolution.Status == EndpointResolutionStatus.Probeable; } /// /// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken. /// public static EndpointResolution Analyze(EndpointCandidate candidate) { if (candidate == null) { return Unresolved("Endpoint-Kandidat fehlt."); } if (candidate.Dynamic) { return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel."); } var address = (candidate.Address ?? string.Empty).Trim(); if (address.Length == 0) { return Unresolved("Aktive statische Transportadresse ist leer."); } if (LooksLikeEmailRecipients(address) || Contains(candidate.AdapterName, "SMTP")) { return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server."); } if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant) || Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal)) { return Expected("Lokaler Dateipfad benoetigt keine Netzwerkprobe."); } if (address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase) || address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase)) { return Expected("Named Pipes besitzen kein TCP-/UDP-Ziel."); } if (address.StartsWith("/", StringComparison.Ordinal) && IsInboundListener(candidate)) { return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung."); } EndpointCatalogEntry entry; string protocol; string host; int port; string reason; if (!TryResolve( address, candidate.AdapterName, IsInboundListener(candidate), out protocol, out host, out port, out reason)) { if (address.StartsWith("file:", StringComparison.OrdinalIgnoreCase) || address.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase)) { return Expected(reason); } return Unresolved(reason); } entry = new EndpointCatalogEntry { Key = candidate.Key, ArtifactType = candidate.ArtifactType, ApplicationName = candidate.ApplicationName, ArtifactName = candidate.ArtifactName, TransportRole = candidate.TransportRole, AdapterName = candidate.AdapterName, Protocol = protocol, Host = host, Port = port, Enabled = true, AutoDiscovered = true }; return new EndpointResolution { Status = EndpointResolutionStatus.Probeable, Entry = entry, Reason = string.Empty }; } /// /// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen. /// public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate) { return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved; } private static bool TryResolve( string address, string adapterName, bool allowWildcardListener, out string protocol, out string host, out int port, out string reason) { protocol = "TCP"; host = string.Empty; port = 0; reason = string.Empty; if (allowWildcardListener && TryResolveWildcardListener(address, out protocol, out host, out port, out reason)) { return true; } if (LooksLikeEmailRecipients(address)) { reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt."; return false; } if (address.StartsWith("\\\\", StringComparison.Ordinal)) { var end = address.IndexOf('\\', 2); host = end < 0 ? address.Substring(2) : address.Substring(2, end - 2); port = 445; return Validate(host, port, out reason); } if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant) || Path.IsPathRooted(address) && !address.StartsWith("/", StringComparison.Ordinal)) { reason = "Lokaler Dateipfad benoetigt keine externe Netzwerkprobe."; return false; } if (TryResolveMsmq(address, out host)) { port = 1801; return Validate(host, port, out reason); } Uri uri; if (Uri.TryCreate(address, UriKind.Absolute, out uri) && !string.IsNullOrWhiteSpace(uri.Scheme)) { var scheme = uri.Scheme.ToLowerInvariant(); if (scheme == "file") { if (string.IsNullOrWhiteSpace(uri.Host)) { reason = "Lokaler file-Endpunkt benoetigt keine externe Netzwerkprobe."; return false; } host = uri.Host; port = 445; return Validate(host, port, out reason); } if (scheme == "net.pipe" || scheme == "npipe") { reason = "Named Pipes sind keine TCP-/UDP-Endpunkte."; return false; } host = uri.Host; protocol = scheme == "udp" ? "UDP" : "TCP"; port = ResolvePort(uri, scheme); if (port <= 0 && HasExplicitPort(address)) { port = uri.Port; } if (port <= 0) { reason = "Fuer das Schema '" + scheme + "' konnte kein TCP-/UDP-Port bestimmt werden."; return false; } return Validate(host, port, out reason); } var match = HostPortPattern.Match(address); if (match.Success) { host = match.Groups["host"].Value.Trim('[', ']'); if (!int.TryParse(match.Groups["port"].Value, out port)) { port = 0; } return Validate(host, port, out reason); } if (Contains(adapterName, "SFTP")) { host = ExtractBareAdapterHost(address); port = 22; return Validate(host, port, out reason); } if (Contains(adapterName, "FTP")) { host = ExtractBareAdapterHost(address); port = 21; return Validate(host, port, out reason); } reason = "Transportadresse enthaelt keinen sicher bestimmbaren Host und Port."; return false; } private static int ResolvePort(Uri uri, string scheme) { if (uri.Port > 0 && !uri.IsDefaultPort) { return uri.Port; } switch (scheme) { case "http": return 80; case "https": return 443; case "ftp": return 21; case "sftp": return 22; case "smb": return 445; case "smtp": return 25; case "ldap": return 389; case "ldaps": return 636; case "pop3": return 110; case "pop3s": return 995; case "imap": return 143; case "imaps": return 993; case "msmq": return 1801; case "net.tcp": return 808; case "mssql": return 1433; // Ohne Pfad ist der Hostteil ein tnsnames.ora-Alias und kein sicher pruefbarer DNS-Host. case "oracledb": return uri.AbsolutePath.Trim('/').Length > 0 ? 1521 : 0; default: return uri.Port > 0 ? uri.Port : 0; } } /// /// Reduziert lokale HTTP-/WCF-Wildcard-Listener auf einen nebenwirkungsfreien Loopback-Porttest. /// private static bool TryResolveWildcardListener( string address, out string protocol, out string host, out int port, out string reason) { protocol = "TCP"; host = string.Empty; port = 0; reason = string.Empty; var match = Regex.Match( address, @"^(?https?|net\.tcp)://(?:\+|\*)(?::(?\d{1,5}))?(?:[/\\].*)?$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (!match.Success) { return false; } var scheme = match.Groups["scheme"].Value.ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(match.Groups["port"].Value) && !int.TryParse(match.Groups["port"].Value, out port)) { port = 0; } if (port <= 0) { port = scheme == "https" ? 443 : scheme == "net.tcp" ? 808 : 80; } host = "127.0.0.1"; return Validate(host, port, out reason); } private static string ExtractBareAdapterHost(string address) { var value = (address ?? string.Empty).Trim().Trim('/'); var separator = value.IndexOfAny(new[] { '/', '\\' }); return separator < 0 ? value : value.Substring(0, separator); } /// /// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt. /// private static bool IsInboundListener(EndpointCandidate candidate) { return string.Equals(candidate.ArtifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase) && (Contains(candidate.AdapterName, "HTTP") || Contains(candidate.AdapterName, "SOAP") || Contains(candidate.AdapterName, "WCF")); } private static EndpointResolution Expected(string reason) { return new EndpointResolution { Status = EndpointResolutionStatus.ExpectedNonProbeable, Reason = reason }; } private static EndpointResolution Unresolved(string reason) { return new EndpointResolution { Status = EndpointResolutionStatus.Unresolved, Reason = string.IsNullOrWhiteSpace(reason) ? "Transportadresse konnte nicht sicher auf Host und Port reduziert werden." : reason }; } private static bool HasExplicitPort(string address) { var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3); var authority = authorityEnd < 0 ? address : address.Substring(0, authorityEnd); return Regex.IsMatch(authority, @":\d{1,5}$", RegexOptions.CultureInvariant); } private static bool TryResolveMsmq(string address, out string host) { host = string.Empty; var match = Regex.Match( address, @"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):(?[^\\/;]+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (!match.Success) { return false; } host = match.Groups["host"].Value.Trim(); return host.Length > 0; } private static bool LooksLikeEmailRecipients(string value) { return value.IndexOf('@') > 0 && value.IndexOf("://", StringComparison.Ordinal) < 0 && !value.StartsWith("\\\\", StringComparison.Ordinal); } private static bool Contains(string value, string fragment) { return !string.IsNullOrWhiteSpace(value) && value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0; } private static bool Validate(string host, int port, out string reason) { host = (host ?? string.Empty).Trim(); if (host.Length == 0 || host.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '*', '%' }) >= 0) { reason = "Host ist leer oder enthaelt nicht aufgeloeste Platzhalter."; return false; } if (port < 1 || port > 65535) { reason = "Port liegt ausserhalb des gueltigen Bereichs."; return false; } reason = string.Empty; return true; } } }