Add BizTalk endpoint reachability monitoring

This commit is contained in:
2026-08-04 09:48:00 +02:00
parent 826d87fef7
commit 91e0db5619
20 changed files with 1714 additions and 38 deletions
@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Liest und schreibt die lokale Endpoint-Konfiguration atomar und mit strikter Validierung.
/// </summary>
internal sealed class EndpointCatalogStore
{
private const string RootName = "BizTalkEndpointCatalog";
private const string Version = "1";
private readonly string _path;
private readonly int _maxBytes;
private readonly int _maxEntries;
public EndpointCatalogStore(string path, int maxBytes, int maxEntries)
{
_path = path;
_maxBytes = maxBytes;
_maxEntries = maxEntries;
}
public EndpointCatalog Read(string environmentName)
{
var info = new FileInfo(_path);
if (!info.Exists)
{
throw new FileNotFoundException("Endpoint catalog does not exist.", _path);
}
if (info.Length <= 0 || info.Length > _maxBytes)
{
throw new InvalidDataException("Endpoint catalog size is outside the configured range.");
}
XDocument document;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersInDocument = _maxBytes
};
using (var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
using (var reader = XmlReader.Create(stream, settings))
{
document = XDocument.Load(reader, LoadOptions.None);
}
var root = document.Root;
if (root == null
|| root.Name.LocalName != RootName
|| ReadAttribute(root, "version") != Version)
{
throw new InvalidDataException("Endpoint catalog format or version is invalid.");
}
var machine = ReadAttribute(root, "machine");
if (!string.Equals(machine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException("Endpoint catalog was created for a different machine.");
}
var catalogEnvironment = ReadAttribute(root, "environment");
if (!string.Equals(catalogEnvironment, environmentName ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException("Endpoint catalog was created for a different environment.");
}
DateTime synchronizedUtc;
if (!DateTime.TryParseExact(
ReadAttribute(root, "synchronizedUtc"),
"o",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out synchronizedUtc))
{
throw new InvalidDataException("Endpoint catalog synchronization timestamp is invalid.");
}
var catalog = new EndpointCatalog
{
MachineName = machine,
EnvironmentName = catalogEnvironment,
SynchronizedUtc = synchronizedUtc,
ActiveCandidates = ReadInt(root, "activeCandidates", 0, _maxEntries),
UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, _maxEntries)
};
foreach (var node in root.Elements("Endpoint"))
{
if (catalog.Entries.Count >= _maxEntries)
{
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount.");
}
var entry = new EndpointCatalogEntry
{
Key = ReadAttribute(node, "key"),
ArtifactType = ReadAttribute(node, "artifactType"),
ApplicationName = ReadAttribute(node, "application"),
ArtifactName = ReadAttribute(node, "artifact"),
TransportRole = ReadAttribute(node, "transportRole"),
AdapterName = ReadAttribute(node, "adapter"),
Protocol = ReadAttribute(node, "protocol").ToUpperInvariant(),
Host = ReadAttribute(node, "host"),
Port = ReadInt(node, "port", 1, 65535),
Enabled = ReadBool(node, "enabled"),
AutoDiscovered = ReadBool(node, "autoDiscovered")
};
ValidateEntry(entry);
catalog.Entries.Add(entry);
}
if (catalog.Entries.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count() != catalog.Entries.Count)
{
throw new InvalidDataException("Endpoint catalog contains duplicate keys.");
}
return catalog;
}
public void Write(EndpointCatalog catalog)
{
if (catalog == null)
{
throw new ArgumentNullException("catalog");
}
if (catalog.Entries.Count > _maxEntries)
{
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount.");
}
foreach (var entry in catalog.Entries)
{
ValidateEntry(entry);
}
var root = new XElement(
RootName,
new XAttribute("version", Version),
new XAttribute("machine", Environment.MachineName),
new XAttribute("environment", catalog.EnvironmentName ?? string.Empty),
new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)),
new XAttribute("activeCandidates", catalog.ActiveCandidates),
new XAttribute("unsupportedCandidates", catalog.UnsupportedCandidates));
foreach (var entry in catalog.Entries
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.ArtifactName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.TransportRole, StringComparer.OrdinalIgnoreCase))
{
root.Add(new XElement(
"Endpoint",
new XAttribute("key", entry.Key),
new XAttribute("artifactType", entry.ArtifactType ?? string.Empty),
new XAttribute("application", entry.ApplicationName ?? string.Empty),
new XAttribute("artifact", entry.ArtifactName ?? string.Empty),
new XAttribute("transportRole", entry.TransportRole ?? string.Empty),
new XAttribute("adapter", entry.AdapterName ?? string.Empty),
new XAttribute("protocol", entry.Protocol),
new XAttribute("host", entry.Host),
new XAttribute("port", entry.Port),
new XAttribute("enabled", entry.Enabled),
new XAttribute("autoDiscovered", entry.AutoDiscovered)));
}
var document = new XDocument(new XDeclaration("1.0", "utf-8", null), root);
byte[] bytes;
using (var memory = new MemoryStream())
using (var writer = XmlWriter.Create(memory, new XmlWriterSettings
{
Encoding = new UTF8Encoding(false),
Indent = true,
NewLineChars = "\r\n",
NewLineHandling = NewLineHandling.Replace
}))
{
document.Save(writer);
writer.Flush();
bytes = memory.ToArray();
}
if (bytes.Length > _maxBytes)
{
throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxBytes.");
}
AtomicWrite(bytes);
}
private void AtomicWrite(byte[] bytes)
{
var directory = Path.GetDirectoryName(_path);
if (string.IsNullOrWhiteSpace(directory))
{
throw new InvalidOperationException("Endpoint catalog path has no parent directory.");
}
Directory.CreateDirectory(directory);
var temporaryPath = Path.Combine(directory, Path.GetFileName(_path) + "." + Guid.NewGuid().ToString("N") + ".tmp");
try
{
using (var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
{
stream.Write(bytes, 0, bytes.Length);
stream.Flush(true);
}
if (File.Exists(_path))
{
File.Replace(temporaryPath, _path, null, true);
}
else
{
File.Move(temporaryPath, _path);
}
}
finally
{
if (File.Exists(temporaryPath))
{
try { File.Delete(temporaryPath); } catch (IOException) { }
}
}
}
private static void ValidateEntry(EndpointCatalogEntry entry)
{
if (entry == null
|| string.IsNullOrWhiteSpace(entry.Key)
|| string.IsNullOrWhiteSpace(entry.ArtifactType)
|| string.IsNullOrWhiteSpace(entry.ArtifactName)
|| string.IsNullOrWhiteSpace(entry.Host)
|| entry.Host.IndexOfAny(new[] { '\r', '\n', '\t', ' ' }) >= 0
|| entry.Port < 1
|| entry.Port > 65535
|| !(string.Equals(entry.Protocol, "TCP", StringComparison.OrdinalIgnoreCase)
|| string.Equals(entry.Protocol, "UDP", StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidDataException("Endpoint catalog contains an invalid entry.");
}
}
private static string ReadAttribute(XElement element, string name)
{
var attribute = element.Attribute(name);
if (attribute == null)
{
throw new InvalidDataException("Endpoint catalog attribute is missing: " + name);
}
return attribute.Value.Trim();
}
private static int ReadInt(XElement element, string name, int min, int max)
{
int value;
if (!int.TryParse(ReadAttribute(element, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out value)
|| value < min
|| value > max)
{
throw new InvalidDataException("Endpoint catalog integer is invalid: " + name);
}
return value;
}
private static bool ReadBool(XElement element, string name)
{
bool value;
if (!bool.TryParse(ReadAttribute(element, name), out value))
{
throw new InvalidDataException("Endpoint catalog boolean is invalid: " + name);
}
return value;
}
}
}