825 lines
32 KiB
C#
825 lines
32 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Xml;
|
|
using System.Xml.Linq;
|
|
using BizTalkSapEnvironmentInventory.Configuration;
|
|
using BizTalkSapEnvironmentInventory.Infrastructure;
|
|
using BizTalkSapEnvironmentInventory.Models;
|
|
|
|
namespace BizTalkSapEnvironmentInventory.Collectors
|
|
{
|
|
/// <summary>
|
|
/// Exportiert BizTalk-Gruppenbindings read-only und extrahiert sichere SAP-Endpunktparameter.
|
|
/// </summary>
|
|
internal sealed class BindingExportCollector
|
|
{
|
|
private static readonly Regex ApplicationModuleName = new Regex(
|
|
@"^\[Application:(.+)\]$",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
|
|
private readonly CommandLineOptions options;
|
|
private readonly InventoryDocument document;
|
|
private readonly ConsoleFileLogger logger;
|
|
private readonly TimeSpan processTimeout;
|
|
|
|
public BindingExportCollector(
|
|
CommandLineOptions options,
|
|
InventoryDocument document,
|
|
ConsoleFileLogger logger,
|
|
int timeoutSeconds)
|
|
{
|
|
this.options = options;
|
|
this.document = document;
|
|
this.logger = logger;
|
|
processTimeout = TimeSpan.FromSeconds(Math.Max(30, timeoutSeconds));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verarbeitet entweder eine vorgegebene Binding-Datei oder einen temporären BTSTask-Export.
|
|
/// </summary>
|
|
public void Collect()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(options.BindingFilePath))
|
|
{
|
|
logger.Info("Werte vorhandenen Binding-Export offline aus: " + options.BindingFilePath);
|
|
ParseBindingDocument(LoadXml(options.BindingFilePath), "Angegebene Binding-Datei");
|
|
return;
|
|
}
|
|
|
|
var btsTask = ResolveBtsTaskPath();
|
|
if (string.IsNullOrWhiteSpace(btsTask))
|
|
{
|
|
throw new FileNotFoundException(
|
|
"BTSTask.exe wurde nicht gefunden. --btstask oder --binding-file verwenden.");
|
|
}
|
|
|
|
var temporaryPath = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"BizTalkSapInventory-" + Guid.NewGuid().ToString("N") + ".xml");
|
|
try
|
|
{
|
|
ExportBindings(btsTask, temporaryPath);
|
|
ParseBindingDocument(LoadXml(temporaryPath), "BTSTask GroupLevel Export");
|
|
}
|
|
finally
|
|
{
|
|
TryDelete(temporaryPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parst ein Binding-Dokument; intern sichtbar, damit der Self-Test reale XML-Strukturen prüft.
|
|
/// </summary>
|
|
internal void ParseBindingDocument(XDocument bindingDocument, string source)
|
|
{
|
|
if (bindingDocument == null || bindingDocument.Root == null)
|
|
{
|
|
throw new InvalidDataException("Binding-XML ist leer.");
|
|
}
|
|
|
|
AddApplicationsFromBinding(bindingDocument);
|
|
|
|
var parsed = 0;
|
|
foreach (var sendPort in bindingDocument.Descendants()
|
|
.Where(item => IsName(item, "SendPort") && HasAncestor(item, "SendPortCollection")))
|
|
{
|
|
var endpoint = ParseSendPort(sendPort, source);
|
|
if (endpoint != null)
|
|
{
|
|
MergeEndpoint(endpoint);
|
|
parsed++;
|
|
}
|
|
}
|
|
|
|
foreach (var receiveLocation in bindingDocument.Descendants()
|
|
.Where(item => IsName(item, "ReceiveLocation")
|
|
&& HasAncestor(item, "ReceiveLocations")))
|
|
{
|
|
var endpoint = ParseReceiveLocation(receiveLocation, source);
|
|
if (endpoint != null)
|
|
{
|
|
MergeEndpoint(endpoint);
|
|
parsed++;
|
|
}
|
|
}
|
|
|
|
logger.Info(parsed + " SAP-Endpunkt(e) aus dem Binding-Export ausgewertet.");
|
|
}
|
|
|
|
private SapEndpointRecord ParseSendPort(XElement sendPort, string source)
|
|
{
|
|
var primaryTransport = Child(sendPort, "PrimaryTransport");
|
|
var transportType = primaryTransport == null ? null : Child(primaryTransport, "TransportType");
|
|
var transportData = primaryTransport == null ? null : Child(primaryTransport, "TransportTypeData");
|
|
var properties = ParsePropertyBag(transportData);
|
|
var adapterName = Attribute(transportType, "Name");
|
|
var address = ChildValue(primaryTransport, "Address");
|
|
|
|
if (!LooksLikeSap(adapterName, address, properties))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var endpoint = new SapEndpointRecord
|
|
{
|
|
ApplicationName = FirstNonEmpty(
|
|
Attribute(sendPort, "ApplicationName"),
|
|
ChildValue(sendPort, "ApplicationName")),
|
|
Direction = "Senden",
|
|
Name = Attribute(sendPort, "Name"),
|
|
ParentPortName = Attribute(sendPort, "Name"),
|
|
AdapterName = adapterName,
|
|
HostName = FirstNonEmpty(
|
|
Attribute(Child(primaryTransport, "SendHandler"), "Name"),
|
|
ChildValue(primaryTransport, "SendHandler")),
|
|
Status = FirstNonEmpty(Attribute(sendPort, "Status"), "Konfiguriert"),
|
|
Address = SensitiveDataSanitizer.SanitizeText(address),
|
|
Source = source
|
|
};
|
|
AddProperties(endpoint, properties);
|
|
ParseSapAddress(endpoint, address);
|
|
FinalizeEndpoint(endpoint);
|
|
return endpoint;
|
|
}
|
|
|
|
private SapEndpointRecord ParseReceiveLocation(XElement receiveLocation, string source)
|
|
{
|
|
var transportType = Child(receiveLocation, "ReceiveLocationTransportType");
|
|
var transportData = Child(receiveLocation, "ReceiveLocationTransportTypeData");
|
|
var properties = ParsePropertyBag(transportData);
|
|
var adapterName = Attribute(transportType, "Name");
|
|
var address = FirstNonEmpty(
|
|
ChildValue(receiveLocation, "Address"),
|
|
Attribute(receiveLocation, "Address"));
|
|
|
|
if (!LooksLikeSap(adapterName, address, properties))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var receivePort = receiveLocation.Ancestors()
|
|
.FirstOrDefault(item => IsName(item, "ReceivePort"));
|
|
var endpoint = new SapEndpointRecord
|
|
{
|
|
ApplicationName = FirstNonEmpty(
|
|
Attribute(receiveLocation, "ApplicationName"),
|
|
ChildValue(receiveLocation, "ApplicationName"),
|
|
Attribute(receivePort, "ApplicationName"),
|
|
ChildValue(receivePort, "ApplicationName")),
|
|
Direction = "Empfangen",
|
|
Name = Attribute(receiveLocation, "Name"),
|
|
ParentPortName = Attribute(receivePort, "Name"),
|
|
AdapterName = adapterName,
|
|
HostName = FirstNonEmpty(
|
|
Attribute(Child(receiveLocation, "ReceiveHandler"), "Name"),
|
|
ChildValue(receiveLocation, "ReceiveHandler")),
|
|
Status = FirstNonEmpty(
|
|
Attribute(receiveLocation, "Enable"),
|
|
Attribute(receiveLocation, "Status"),
|
|
"Konfiguriert"),
|
|
Address = SensitiveDataSanitizer.SanitizeText(address),
|
|
Source = source
|
|
};
|
|
AddProperties(endpoint, properties);
|
|
ParseSapAddress(endpoint, address);
|
|
FinalizeEndpoint(endpoint);
|
|
return endpoint;
|
|
}
|
|
|
|
private static List<NameValueRecord> ParsePropertyBag(XElement dataElement)
|
|
{
|
|
var result = new List<NameValueRecord>();
|
|
if (dataElement == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var raw = dataElement.HasElements
|
|
? string.Concat(dataElement.Nodes().Select(item => item.ToString(SaveOptions.DisableFormatting)))
|
|
: dataElement.Value;
|
|
ParseXmlProperties(raw, "Binding", result, 0);
|
|
return result;
|
|
}
|
|
|
|
private static void ParseXmlProperties(
|
|
string raw,
|
|
string source,
|
|
List<NameValueRecord> target,
|
|
int depth)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw) || depth > 4)
|
|
{
|
|
return;
|
|
}
|
|
|
|
raw = WebUtility.HtmlDecode(raw.Trim());
|
|
if (!raw.StartsWith("<", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
XElement root;
|
|
try
|
|
{
|
|
root = XElement.Parse(raw, LoadOptions.None);
|
|
}
|
|
catch (XmlException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var element in root.DescendantsAndSelf())
|
|
{
|
|
foreach (var attribute in element.Attributes())
|
|
{
|
|
if (string.Equals(attribute.Name.LocalName, "vt", StringComparison.OrdinalIgnoreCase)
|
|
|| attribute.IsNamespaceDeclaration)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddUnique(
|
|
target,
|
|
element.Name.LocalName + "." + attribute.Name.LocalName,
|
|
SensitiveDataSanitizer.RedactValue(attribute.Name.LocalName, attribute.Value),
|
|
source);
|
|
AddUnique(
|
|
target,
|
|
NormalizePropertyName(attribute.Name.LocalName),
|
|
SensitiveDataSanitizer.RedactValue(attribute.Name.LocalName, attribute.Value),
|
|
source);
|
|
}
|
|
|
|
if (element.HasElements)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var name = element.Name.LocalName;
|
|
if (!SensitiveDataSanitizer.IsSensitiveName(name)
|
|
&& element.Value.TrimStart().StartsWith("<", StringComparison.Ordinal))
|
|
{
|
|
ParseXmlProperties(element.Value, name, target, depth + 1);
|
|
continue;
|
|
}
|
|
|
|
var value = SensitiveDataSanitizer.RedactValue(name, element.Value);
|
|
AddUnique(target, name, value, source);
|
|
}
|
|
}
|
|
|
|
private static void ParseSapAddress(SapEndpointRecord endpoint, string rawAddress)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(rawAddress)
|
|
|| !rawAddress.StartsWith("sap://", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var address = SensitiveDataSanitizer.SanitizeText(rawAddress);
|
|
var withoutScheme = address.Substring("sap://".Length);
|
|
var atIndex = withoutScheme.IndexOf('@');
|
|
var userInfo = atIndex >= 0 ? withoutScheme.Substring(0, atIndex) : string.Empty;
|
|
var hostAndQuery = atIndex >= 0 ? withoutScheme.Substring(atIndex + 1) : withoutScheme;
|
|
|
|
foreach (var item in userInfo.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var pair = item.Split(new[] { '=' }, 2);
|
|
if (pair.Length == 2)
|
|
{
|
|
AddUnique(
|
|
endpoint.Properties,
|
|
NormalizePropertyName(pair[0]),
|
|
Decode(pair[1]),
|
|
"SAP-URI");
|
|
}
|
|
}
|
|
|
|
var queryIndex = hostAndQuery.IndexOf('?');
|
|
var hostPart = queryIndex >= 0
|
|
? hostAndQuery.Substring(0, queryIndex)
|
|
: hostAndQuery;
|
|
var query = queryIndex >= 0
|
|
? hostAndQuery.Substring(queryIndex + 1)
|
|
: string.Empty;
|
|
|
|
var hostSegments = hostPart.Split('/');
|
|
if (hostSegments.Length > 0)
|
|
{
|
|
AddUnique(endpoint.Properties, "ConnectionType", Decode(hostSegments[0]), "SAP-URI");
|
|
if (string.Equals(hostSegments[0], "A", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (hostSegments.Length > 1)
|
|
{
|
|
AddUnique(endpoint.Properties, "ApplicationServerHost", Decode(hostSegments[1]), "SAP-URI");
|
|
}
|
|
if (hostSegments.Length > 2)
|
|
{
|
|
AddUnique(endpoint.Properties, "SystemNumber", Decode(hostSegments[2]), "SAP-URI");
|
|
}
|
|
}
|
|
else if (string.Equals(hostSegments[0], "B", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (hostSegments.Length > 1)
|
|
{
|
|
AddUnique(endpoint.Properties, "MessageServerHost", Decode(hostSegments[1]), "SAP-URI");
|
|
}
|
|
if (hostSegments.Length > 2)
|
|
{
|
|
AddUnique(endpoint.Properties, "R3SystemName", Decode(hostSegments[2]), "SAP-URI");
|
|
}
|
|
}
|
|
else if (string.Equals(hostSegments[0], "D", StringComparison.OrdinalIgnoreCase)
|
|
&& hostSegments.Length > 1)
|
|
{
|
|
AddUnique(endpoint.Properties, "DestinationName", Decode(hostSegments[1]), "SAP-URI");
|
|
}
|
|
}
|
|
|
|
foreach (var item in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var pair = item.Split(new[] { '=' }, 2);
|
|
if (pair.Length == 2)
|
|
{
|
|
AddUnique(
|
|
endpoint.Properties,
|
|
NormalizePropertyName(Decode(pair[0])),
|
|
SensitiveDataSanitizer.RedactValue(pair[0], Decode(pair[1])),
|
|
"SAP-URI");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FinalizeEndpoint(SapEndpointRecord endpoint)
|
|
{
|
|
ResolveApplication(endpoint);
|
|
|
|
var useSnc = endpoint.GetProperty("UseSnc", "UseSNC");
|
|
var sncEnabled = IsTrue(useSnc);
|
|
var affiliateApplication = endpoint.GetProperty(
|
|
"AffiliateApplicationName",
|
|
"SsoAffiliateApplication",
|
|
"SSOApplication");
|
|
var userName = endpoint.GetProperty("UserName", "Username");
|
|
|
|
if (sncEnabled)
|
|
{
|
|
endpoint.SecurityMode = "SAP SNC";
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(affiliateApplication))
|
|
{
|
|
endpoint.SecurityMode = "Enterprise SSO";
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(userName))
|
|
{
|
|
endpoint.SecurityMode = "SAP-Benutzer";
|
|
}
|
|
else
|
|
{
|
|
endpoint.SecurityMode = "Nicht eindeutig aus Binding ableitbar";
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(affiliateApplication))
|
|
{
|
|
endpoint.CredentialReference = "SSO Affiliate Application: " + affiliateApplication;
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(userName))
|
|
{
|
|
endpoint.CredentialReference = "SAP-Benutzer: " + userName
|
|
+ "; Kennwort wird nicht exportiert/dokumentiert.";
|
|
}
|
|
else
|
|
{
|
|
endpoint.CredentialReference =
|
|
"Keine Kennwortinformation im Binding-Export; Credentials separat kontrollieren.";
|
|
}
|
|
|
|
foreach (var property in endpoint.Properties.Where(item =>
|
|
item.Name.IndexOf("Action", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| item.Value.IndexOf("Microsoft.LobServices.Sap", StringComparison.OrdinalIgnoreCase) >= 0))
|
|
{
|
|
var operation = SensitiveDataSanitizer.SanitizeText(property.Value);
|
|
if (!string.IsNullOrWhiteSpace(operation)
|
|
&& !endpoint.Operations.Contains(operation, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
endpoint.Operations.Add(operation);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ResolveApplication(SapEndpointRecord endpoint)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(endpoint.ApplicationName))
|
|
{
|
|
EnsureApplication(endpoint.ApplicationName);
|
|
return;
|
|
}
|
|
|
|
foreach (var application in document.Applications)
|
|
{
|
|
if (application.Artifacts.Any(item =>
|
|
string.Equals(item.Name, endpoint.Name, StringComparison.OrdinalIgnoreCase)
|
|
|| (!string.IsNullOrWhiteSpace(endpoint.ParentPortName)
|
|
&& string.Equals(
|
|
item.Name,
|
|
endpoint.ParentPortName,
|
|
StringComparison.OrdinalIgnoreCase))))
|
|
{
|
|
endpoint.ApplicationName = application.Name;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MergeEndpoint(SapEndpointRecord parsed)
|
|
{
|
|
var existing = document.SapEndpoints.FirstOrDefault(item =>
|
|
string.Equals(item.Direction, parsed.Direction, StringComparison.OrdinalIgnoreCase)
|
|
&& string.Equals(item.Name, parsed.Name, StringComparison.OrdinalIgnoreCase));
|
|
if (existing == null)
|
|
{
|
|
document.SapEndpoints.Add(parsed);
|
|
return;
|
|
}
|
|
|
|
existing.ApplicationName = FirstNonEmpty(parsed.ApplicationName, existing.ApplicationName);
|
|
existing.ParentPortName = FirstNonEmpty(parsed.ParentPortName, existing.ParentPortName);
|
|
existing.AdapterName = FirstNonEmpty(parsed.AdapterName, existing.AdapterName);
|
|
existing.HostName = FirstNonEmpty(parsed.HostName, existing.HostName);
|
|
existing.Status = FirstNonEmpty(parsed.Status, existing.Status);
|
|
existing.Address = FirstNonEmpty(parsed.Address, existing.Address);
|
|
existing.SecurityMode = FirstNonEmpty(parsed.SecurityMode, existing.SecurityMode);
|
|
existing.CredentialReference = FirstNonEmpty(
|
|
parsed.CredentialReference,
|
|
existing.CredentialReference);
|
|
existing.Source = FirstNonEmpty(parsed.Source, existing.Source);
|
|
foreach (var property in parsed.Properties)
|
|
{
|
|
AddUnique(existing.Properties, property.Name, property.Value, property.Source);
|
|
}
|
|
foreach (var operation in parsed.Operations)
|
|
{
|
|
if (!existing.Operations.Contains(operation, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
existing.Operations.Add(operation);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddApplicationsFromBinding(XDocument bindingDocument)
|
|
{
|
|
foreach (var module in bindingDocument.Descendants().Where(item => IsName(item, "ModuleRef")))
|
|
{
|
|
var match = ApplicationModuleName.Match(Attribute(module, "Name"));
|
|
if (match.Success)
|
|
{
|
|
EnsureApplication(match.Groups[1].Value);
|
|
}
|
|
}
|
|
|
|
foreach (var applicationName in bindingDocument.Descendants()
|
|
.Where(item => IsName(item, "ApplicationName"))
|
|
.Select(item => item.Value)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
EnsureApplication(applicationName);
|
|
}
|
|
}
|
|
|
|
private void EnsureApplication(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)
|
|
|| document.Applications.Any(item =>
|
|
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
document.Applications.Add(new ApplicationRecord
|
|
{
|
|
Name = name.Trim(),
|
|
Status = "Aus Binding-Export erkannt"
|
|
});
|
|
}
|
|
|
|
private void ExportBindings(string btsTaskPath, string destination)
|
|
{
|
|
var arguments = new StringBuilder();
|
|
arguments.Append("ExportBindings /GroupLevel /Destination:\"")
|
|
.Append(EscapeCommandArgument(destination))
|
|
.Append("\"");
|
|
if (!string.IsNullOrWhiteSpace(document.System.ManagementServer))
|
|
{
|
|
arguments.Append(" /Server:\"")
|
|
.Append(EscapeCommandArgument(document.System.ManagementServer))
|
|
.Append("\"");
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(document.System.ManagementDatabase))
|
|
{
|
|
arguments.Append(" /Database:\"")
|
|
.Append(EscapeCommandArgument(document.System.ManagementDatabase))
|
|
.Append("\"");
|
|
}
|
|
|
|
logger.Info("Exportiere BizTalk-Gruppenbindings read-only mit BTSTask.");
|
|
logger.Detail("BTSTask-Pfad: " + btsTaskPath);
|
|
logger.Detail("BTSTask-Timeout: "
|
|
+ processTimeout.TotalSeconds.ToString(CultureInfo.InvariantCulture)
|
|
+ " Sekunden.");
|
|
var output = new StringBuilder();
|
|
var outputSync = new object();
|
|
var timer = Stopwatch.StartNew();
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = btsTaskPath,
|
|
Arguments = arguments.ToString(),
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
WorkingDirectory = Path.GetDirectoryName(btsTaskPath)
|
|
};
|
|
|
|
using (var process = new Process { StartInfo = startInfo })
|
|
{
|
|
process.OutputDataReceived += (sender, args) =>
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(args.Data))
|
|
{
|
|
var safeLine = SensitiveDataSanitizer.SanitizeText(args.Data);
|
|
lock (outputSync)
|
|
{
|
|
output.AppendLine(safeLine);
|
|
}
|
|
logger.Detail("BTSTask: " + safeLine);
|
|
}
|
|
};
|
|
process.ErrorDataReceived += (sender, args) =>
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(args.Data))
|
|
{
|
|
var safeLine = SensitiveDataSanitizer.SanitizeText(args.Data);
|
|
lock (outputSync)
|
|
{
|
|
output.AppendLine(safeLine);
|
|
}
|
|
logger.Detail("BTSTask: " + safeLine);
|
|
}
|
|
};
|
|
|
|
process.Start();
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
if (!process.WaitForExit((int)processTimeout.TotalMilliseconds))
|
|
{
|
|
try
|
|
{
|
|
process.Kill();
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
}
|
|
throw new TimeoutException(
|
|
"BTSTask ExportBindings überschritt " + processTimeout.TotalSeconds + " Sekunden.");
|
|
}
|
|
process.WaitForExit();
|
|
timer.Stop();
|
|
if (process.ExitCode != 0)
|
|
{
|
|
string errorOutput;
|
|
lock (outputSync)
|
|
{
|
|
errorOutput = output.ToString();
|
|
}
|
|
throw new InvalidOperationException(
|
|
"BTSTask ExportBindings meldete Exitcode "
|
|
+ process.ExitCode
|
|
+ ": "
|
|
+ LastLines(errorOutput, 8));
|
|
}
|
|
logger.Info("BTSTask ExportBindings erfolgreich abgeschlossen ("
|
|
+ timer.ElapsedMilliseconds + " ms).");
|
|
}
|
|
|
|
if (!File.Exists(destination) || new FileInfo(destination).Length == 0)
|
|
{
|
|
throw new InvalidDataException("BTSTask erzeugte keine Binding-Datei.");
|
|
}
|
|
logger.Detail("Temporärer Binding-Export: "
|
|
+ new FileInfo(destination).Length
|
|
.ToString(CultureInfo.InvariantCulture)
|
|
+ " Bytes.");
|
|
}
|
|
|
|
private string ResolveBtsTaskPath()
|
|
{
|
|
var candidates = new List<string>();
|
|
if (!string.IsNullOrWhiteSpace(options.BtsTaskPath))
|
|
{
|
|
candidates.Add(options.BtsTaskPath);
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath))
|
|
{
|
|
candidates.Add(Path.Combine(document.System.BizTalkInstallPath, "BTSTask.exe"));
|
|
}
|
|
|
|
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
|
candidates.Add(Path.Combine(programFiles, "Microsoft BizTalk Server 2020", "BTSTask.exe"));
|
|
candidates.Add(Path.Combine(programFilesX86, "Microsoft BizTalk Server 2020", "BTSTask.exe"));
|
|
|
|
var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
|
candidates.AddRange(path.Split(Path.PathSeparator)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
|
.Select(item => Path.Combine(item.Trim(), "BTSTask.exe")));
|
|
|
|
return candidates.FirstOrDefault(File.Exists);
|
|
}
|
|
|
|
private static XDocument LoadXml(string path)
|
|
{
|
|
var settings = new XmlReaderSettings
|
|
{
|
|
DtdProcessing = DtdProcessing.Prohibit,
|
|
XmlResolver = null,
|
|
IgnoreComments = true
|
|
};
|
|
using (var stream = File.OpenRead(path))
|
|
using (var reader = XmlReader.Create(stream, settings))
|
|
{
|
|
return XDocument.Load(reader, LoadOptions.None);
|
|
}
|
|
}
|
|
|
|
private static bool LooksLikeSap(
|
|
string adapterName,
|
|
string address,
|
|
IEnumerable<NameValueRecord> properties)
|
|
{
|
|
if ((adapterName ?? string.Empty).IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| (address ?? string.Empty).StartsWith("sap://", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return properties.Any(item =>
|
|
item.Name.IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| item.Value.IndexOf("sapBinding", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| item.Value.IndexOf("Microsoft.Adapters.SAP", StringComparison.OrdinalIgnoreCase) >= 0);
|
|
}
|
|
|
|
private static void AddProperties(
|
|
SapEndpointRecord endpoint,
|
|
IEnumerable<NameValueRecord> properties)
|
|
{
|
|
foreach (var property in properties)
|
|
{
|
|
AddUnique(endpoint.Properties, property.Name, property.Value, property.Source);
|
|
}
|
|
}
|
|
|
|
private static void AddUnique(
|
|
List<NameValueRecord> target,
|
|
string name,
|
|
string value,
|
|
string source)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var safeValue = SensitiveDataSanitizer.RedactValue(name, value);
|
|
if (target.Any(item =>
|
|
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)
|
|
&& string.Equals(item.Value, safeValue, StringComparison.Ordinal)))
|
|
{
|
|
return;
|
|
}
|
|
target.Add(new NameValueRecord(name, safeValue, source));
|
|
}
|
|
|
|
private static string NormalizePropertyName(string name)
|
|
{
|
|
var normalized = (name ?? string.Empty).Trim();
|
|
var mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "Client", "Client" },
|
|
{ "lang", "Language" },
|
|
{ "Language", "Language" },
|
|
{ "GwHost", "GatewayHost" },
|
|
{ "GwServ", "GatewayService" },
|
|
{ "ListenerDest", "ListenerDestination" },
|
|
{ "ListenerGwHost", "ListenerGatewayHost" },
|
|
{ "ListenerGwServ", "ListenerGatewayService" },
|
|
{ "ListenerProgramId", "ListenerProgramId" },
|
|
{ "UseSnc", "UseSnc" },
|
|
{ "SAPROUTER", "SapRouter" }
|
|
};
|
|
string mapped;
|
|
return mappings.TryGetValue(normalized, out mapped) ? mapped : normalized;
|
|
}
|
|
|
|
private static XElement Child(XElement parent, string localName)
|
|
{
|
|
return parent == null
|
|
? null
|
|
: parent.Elements().FirstOrDefault(item => IsName(item, localName));
|
|
}
|
|
|
|
private static string ChildValue(XElement parent, string localName)
|
|
{
|
|
var child = Child(parent, localName);
|
|
return child == null ? string.Empty : child.Value;
|
|
}
|
|
|
|
private static string Attribute(XElement element, string localName)
|
|
{
|
|
if (element == null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
var attribute = element.Attributes().FirstOrDefault(item =>
|
|
string.Equals(item.Name.LocalName, localName, StringComparison.OrdinalIgnoreCase));
|
|
return attribute == null ? string.Empty : attribute.Value;
|
|
}
|
|
|
|
private static bool IsName(XElement element, string localName)
|
|
{
|
|
return string.Equals(
|
|
element.Name.LocalName,
|
|
localName,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool HasAncestor(XElement element, string localName)
|
|
{
|
|
return element.Ancestors().Any(item => IsName(item, localName));
|
|
}
|
|
|
|
private static string FirstNonEmpty(params string[] values)
|
|
{
|
|
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
|
|
}
|
|
|
|
private static bool IsTrue(string value)
|
|
{
|
|
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(value, "ja", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static string Decode(string value)
|
|
{
|
|
try
|
|
{
|
|
return Uri.UnescapeDataString((value ?? string.Empty).Replace("+", " "));
|
|
}
|
|
catch (UriFormatException)
|
|
{
|
|
return value ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
private static string EscapeCommandArgument(string value)
|
|
{
|
|
return (value ?? string.Empty).Replace("\"", string.Empty)
|
|
.Replace("\r", string.Empty)
|
|
.Replace("\n", string.Empty);
|
|
}
|
|
|
|
private static string LastLines(string text, int count)
|
|
{
|
|
var lines = (text ?? string.Empty)
|
|
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
|
return string.Join(" | ", lines.Skip(Math.Max(0, lines.Length - count)));
|
|
}
|
|
|
|
private static void TryDelete(string path)
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|