diff --git a/Dokumentation.md b/Dokumentation.md index 53e7661..d977800 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -249,8 +249,12 @@ Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfängerlisten und Named Pipes sowie relative HTTP-/WCF-Receive-Listener werden als erwartbar nicht socket-prüfbar behandelt und erzeugen kein `UNKNOWN`. WCF-SQL-Adressen im Format `mssql://server[:port]/instanz/datenbank` werden auf TCP 1433 oder -den expliziten Port reduziert. Echte Auflösungslücken bleiben sichtbar und -nennen Artefakt, Adapter und Grund, aber niemals die vollständige URI. +den expliziten Port reduziert. Schemafreie `host:port`-Angaben werden vor der +allgemeinen URI-Auswertung erkannt. Bei FTP und SFTP werden zusätzlich Angaben +der Form `[benutzer@]host[:port]/pfad` auf Host und expliziten beziehungsweise +adaptertypischen Standardport reduziert. Echte Auflösungslücken bleiben +sichtbar und nennen Artefakt, Adapter und Grund, aber niemals die vollständige +URI. Der lokale Katalog `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten diff --git a/README.md b/README.md index 7970915..b24893e 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ Der Provider nutzt für die Endpoint-Discovery keine neue WMI-Klasse. Er liest reiner Host/Port-Test: HTTP(S), SFTP, FTP, WCF/net.tcp, WCF-SQL/mssql und UNC werden per TCP geprüft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden keine HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet. +Auch schemafreie `host:port`-Angaben sowie FTP-/SFTP-Adressen der Form +`[benutzer@]host[:port]/pfad` werden erkannt; Benutzer und Pfad werden dabei +nicht in den Endpoint-Katalog übernommen. Nicht jede aktive BizTalk-Adresse ist selbst ein Socket-Ziel. Dynamische Send Ports, lokale Pfade, SMTP-Empfänger, Named Pipes und relative HTTP-/WCF- diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index 06a04c2..ae8cb82 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -183,7 +183,8 @@ Zweck: - Reduziert die Adresse auf Host/Port und führt keinen fachlichen HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus. - Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL- - `mssql`-URIs und lokale HTTP-/WCF-Wildcard-Listener. + `mssql`-URIs, schemafreie `host:port`-Angaben, FTP-/SFTP-Ziele mit optionalen + Benutzerinformationen sowie lokale HTTP-/WCF-Wildcard-Listener. - Behandelt Adressen ohne eigenständiges Socket-Ziel (zum Beispiel lokale Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als erwartbar ausgenommen und nicht als Abdeckungsfehler. diff --git a/docs/EndpointCatalog.md b/docs/EndpointCatalog.md index dd1755b..4d88be9 100644 --- a/docs/EndpointCatalog.md +++ b/docs/EndpointCatalog.md @@ -65,6 +65,14 @@ geschrieben. Der Katalog liegt im bereits ACL-geschützten `data`-Verzeichnis; nur Provider und Administratoren dürfen ihn ändern. `LocalSystem` liest nur den separaten, validierten Checkmk-Snapshot. +Die automatische Auflösung akzeptiert neben absoluten URIs auch `host:port` +und bei FTP/SFTP die schemafreie Form `[benutzer@]host[:port]/pfad`. Die +Reihenfolge ist absichtlich eindeutig: `host:port` wird vor der allgemeinen +.NET-URI-Auswertung erkannt, damit der Host nicht fälschlich als URI-Schema +gedeutet wird. Benutzerinformationen und Pfad bleiben ausschließlich im +flüchtigen WMI-Kandidaten und werden nicht persistiert. Eckige URI-Klammern +um IPv6-Adressen werden vor der Socket-Probe entfernt. + ## Manuelle Korrektur eines automatisch nicht auflösbaren Endpoints Vor einer manuellen Änderung den Scheduled Task kurz stoppen und die Datei diff --git a/src/BizTalkCheckmkPulse/EndpointAddressParser.cs b/src/BizTalkCheckmkPulse/EndpointAddressParser.cs index 203f616..a361e3f 100644 --- a/src/BizTalkCheckmkPulse/EndpointAddressParser.cs +++ b/src/BizTalkCheckmkPulse/EndpointAddressParser.cs @@ -48,7 +48,16 @@ namespace BizTalkCheckmkPulse return Unresolved("Aktive statische Transportadresse ist leer."); } - if (LooksLikeEmailRecipients(address) || Contains(candidate.AdapterName, "SMTP")) + if (Contains(candidate.AdapterName, "SMTP")) + { + return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server."); + } + + // SFTP-/FTP-Adressen duerfen Benutzerinformationen enthalten (user@host). + // Deshalb erst nach der adapterspezifischen Aufloesung als Empfaengerliste einstufen. + if (LooksLikeEmailRecipients(address) + && !Contains(candidate.AdapterName, "SFTP") + && !Contains(candidate.AdapterName, "FTP")) { return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server."); } @@ -144,7 +153,9 @@ namespace BizTalkCheckmkPulse return true; } - if (LooksLikeEmailRecipients(address)) + if (LooksLikeEmailRecipients(address) + && !Contains(adapterName, "SFTP") + && !Contains(adapterName, "FTP")) { reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt."; return false; @@ -171,6 +182,28 @@ namespace BizTalkCheckmkPulse return Validate(host, port, out reason); } + // Host:Port muss vor Uri.TryCreate ausgewertet werden: .NET interpretiert + // beispielsweise "server:2222/path" sonst als URI mit dem Schema "server". + 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); + } + + // BizTalk-FTP-/SFTP-Konfigurationen kommen auch ohne Schema vor. Durch + // die temporaere URI-Normalisierung werden user@host, IPv6 und Ports + // sauber getrennt; Benutzername und Pfad gelangen nicht in den Katalog. + if (TryResolveAdapterUri(address, adapterName, out host, out port)) + { + return Validate(host, port, out reason); + } + Uri uri; if (Uri.TryCreate(address, UriKind.Absolute, out uri) && !string.IsNullOrWhiteSpace(uri.Scheme)) @@ -184,7 +217,7 @@ namespace BizTalkCheckmkPulse return false; } - host = uri.Host; + host = NormalizeHost(uri.Host); port = 445; return Validate(host, port, out reason); } @@ -195,7 +228,7 @@ namespace BizTalkCheckmkPulse return false; } - host = uri.Host; + host = NormalizeHost(uri.Host); protocol = scheme == "udp" ? "UDP" : "TCP"; port = ResolvePort(uri, scheme); if (port <= 0 && HasExplicitPort(address)) @@ -212,18 +245,6 @@ namespace BizTalkCheckmkPulse 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); @@ -318,6 +339,33 @@ namespace BizTalkCheckmkPulse return separator < 0 ? value : value.Substring(0, separator); } + private static bool TryResolveAdapterUri(string address, string adapterName, out string host, out int port) + { + host = string.Empty; + port = 0; + var scheme = Contains(adapterName, "SFTP") + ? "sftp" + : Contains(adapterName, "FTP") ? "ftp" : string.Empty; + if (scheme.Length == 0 || address.IndexOf("://", StringComparison.Ordinal) >= 0) + { + return false; + } + + Uri uri; + var normalized = scheme + "://" + address.TrimStart('/'); + if (!Uri.TryCreate(normalized, UriKind.Absolute, out uri) + || string.IsNullOrWhiteSpace(uri.Host)) + { + return false; + } + + host = NormalizeHost(uri.Host); + port = uri.IsDefaultPort || uri.Port <= 0 + ? scheme == "sftp" ? 22 : 21 + : uri.Port; + return true; + } + /// /// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt. /// @@ -385,10 +433,16 @@ namespace BizTalkCheckmkPulse && value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0; } + private static string NormalizeHost(string host) + { + return (host ?? string.Empty).Trim().Trim('[', ']'); + } + 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) + host = NormalizeHost(host); + if (host.Length == 0 + || host.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '*', '%', '/', '\\', '@', ';', '?', '#' }) >= 0) { reason = "Host ist leer oder enthaelt nicht aufgeloeste Platzhalter."; return false; diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index ac0b23d..cbd3538 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -405,6 +405,31 @@ namespace BizTalkCheckmkPulse.Tests Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "net.tcp URI should resolve: " + reason); AssertEqual(808, endpoint.Port, "net.tcp default port"); + candidate.Address = "transfer.example.test:2222/outbound"; + candidate.AdapterName = "Custom Adapter"; + Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "plain host:port should resolve before URI parsing: " + reason); + AssertEqual("transfer.example.test", endpoint.Host, "plain host:port host"); + AssertEqual(2222, endpoint.Port, "plain host:port port"); + + candidate.Address = "integration-user@sftp.example.test/inbound"; + candidate.AdapterName = "SFTP"; + Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "schema-less SFTP URI with user info should resolve: " + reason); + AssertEqual("sftp.example.test", endpoint.Host, "schema-less SFTP host"); + AssertEqual(22, endpoint.Port, "schema-less SFTP default port"); + Assert(endpoint.Host.IndexOf("integration-user", StringComparison.OrdinalIgnoreCase) < 0, "SFTP user info must not enter catalog host"); + + candidate.Address = "ftp-user@ftp.example.test:2121/drop"; + candidate.AdapterName = "FTP"; + Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "schema-less FTP URI with user info should resolve: " + reason); + AssertEqual("ftp.example.test", endpoint.Host, "schema-less FTP host"); + AssertEqual(2121, endpoint.Port, "schema-less FTP explicit port"); + + candidate.Address = "sftp://[2001:db8::10]:2222/inbound"; + candidate.AdapterName = "SFTP"; + Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "bracketed IPv6 URI should resolve: " + reason); + AssertEqual("2001:db8::10", endpoint.Host, "IPv6 catalog host must not retain URI brackets"); + AssertEqual(2222, endpoint.Port, "IPv6 explicit port"); + candidate.Address = "oracledb://oracle01/ORDERS/Dedicated"; candidate.AdapterName = "WCF-OracleDB"; Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "direct Oracle DB URI should resolve: " + reason);