289 lines
11 KiB
C#
289 lines
11 KiB
C#
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, 100000),
|
|
UnresolvedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000)
|
|
};
|
|
|
|
foreach (var node in root.Elements("Endpoint"))
|
|
{
|
|
if (catalog.Entries.Count >= _maxEntries)
|
|
{
|
|
throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
|
|
}
|
|
|
|
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 the configured entry limit.");
|
|
}
|
|
|
|
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),
|
|
// Der XML-Attributsname bleibt fuer vorhandene Katalogdateien der Version 1 stabil.
|
|
new XAttribute("unsupportedCandidates", catalog.UnresolvedCandidates));
|
|
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;
|
|
}
|
|
}
|
|
}
|