408 lines
14 KiB
C#
408 lines
14 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace BizTalkCheckmkPulse
|
|
{
|
|
/// <summary>
|
|
/// Reduziert adapter-spezifische BizTalk-Adressen auf einen geheimnisfreien Host/Port-Test.
|
|
/// </summary>
|
|
internal static class EndpointAddressParser
|
|
{
|
|
private static readonly Regex HostPortPattern = new Regex(
|
|
@"^(?<host>\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?<port>\d{1,5})(?:[/\\].*)?$",
|
|
RegexOptions.CultureInvariant);
|
|
|
|
/// <summary>
|
|
/// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
|
|
/// </summary>
|
|
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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reduziert lokale HTTP-/WCF-Wildcard-Listener auf einen nebenwirkungsfreien Loopback-Porttest.
|
|
/// </summary>
|
|
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,
|
|
@"^(?<scheme>https?|net\.tcp)://(?:\+|\*)(?::(?<port>\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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt.
|
|
/// </summary>
|
|
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):(?<host>[^\\/;]+)",
|
|
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;
|
|
}
|
|
}
|
|
}
|