|
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace System.Security.Authentication.ExtendedProtection
{
// derived from ReadOnlyCollectionBase because it needs to be back ported to .Net 1.x
[SuppressMessage("Microsoft.Design","CA1058:TypesShouldNotExtendCertainBaseTypes", Justification="changing this would be a breaking change; this code has already shipped")]
[Serializable]
public class ServiceNameCollection : ReadOnlyCollectionBase
{
public ServiceNameCollection(ICollection items)
{
if (items == null) {
throw new ArgumentNullException("items");
}
// Normalize and filter for duplicates
foreach (string serviceName in items)
{
AddIfNew(InnerList, serviceName);
}
}
public ServiceNameCollection Merge(string serviceName)
{
ArrayList newServiceNames = new ArrayList(); // be compatible with .Net 1.x; no generics
newServiceNames.AddRange(this.InnerList);
AddIfNew(newServiceNames, serviceName);
ServiceNameCollection newCollection = new ServiceNameCollection(newServiceNames);
return newCollection;
}
public ServiceNameCollection Merge(IEnumerable serviceNames)
{
ArrayList newServiceNames = new ArrayList(); // be compatible with .Net 1.x; no generics
newServiceNames.AddRange(this.InnerList);
// we have a pretty bad performance here: O(n^2), but since service name lists should
// be small (<<50) and Merge() should not be called frequently, this shouldn't be an issue
foreach (object item in serviceNames) {
AddIfNew(newServiceNames, item as string);
}
ServiceNameCollection newCollection = new ServiceNameCollection(newServiceNames);
return newCollection;
}
// Normalize, check for duplicates, and add if the value is unique
private static void AddIfNew(ArrayList newServiceNames, string serviceName)
{
if (String.IsNullOrEmpty(serviceName)) {
throw new ArgumentException(SR.GetString(SR.security_ServiceNameCollection_EmptyServiceName));
}
serviceName = NormalizeServiceName(serviceName);
if (!Contains(serviceName, newServiceNames)) {
newServiceNames.Add(serviceName);
}
}
// Assumes searchServiceName and serviceNames have already been normalized
internal static bool Contains(string searchServiceName, ICollection serviceNames)
{
Debug.Assert(serviceNames != null);
Debug.Assert(!String.IsNullOrEmpty(searchServiceName));
foreach (string serviceName in serviceNames) {
if (Match(serviceName, searchServiceName)) {
return true;
}
}
return false;
}
public bool Contains(string searchServiceName)
{
string searchName = NormalizeServiceName(searchServiceName);
return Contains(searchName, InnerList);
}
// Normalizes any punycode to unicode in an Service Name (SPN) host.
// If the algorithm fails at any point then the original input is returned.
// ServiceName is in one of the following forms:
// prefix/host
// prefix/host:port
// prefix/host/DistinguishedName
// prefix/host:port/DistinguishedName
internal static string NormalizeServiceName(string inputServiceName)
{
if (string.IsNullOrWhiteSpace(inputServiceName))
{
return inputServiceName;
}
// Separate out the prefix
int shashIndex = inputServiceName.IndexOf('/');
if (shashIndex < 0)
{
return inputServiceName;
}
string prefix = inputServiceName.Substring(0, shashIndex + 1); // Includes slash
string hostPortAndDistinguisher = inputServiceName.Substring(shashIndex + 1); // Excludes slash
if (string.IsNullOrWhiteSpace(hostPortAndDistinguisher))
{
return inputServiceName;
}
string host = hostPortAndDistinguisher;
string port = string.Empty;
string distinguisher = string.Empty;
// Check for the absence of a port or distinguisher.
UriHostNameType hostType = Uri.CheckHostName(hostPortAndDistinguisher);
if (hostType == UriHostNameType.Unknown)
{
string hostAndPort = hostPortAndDistinguisher;
// Check for distinguisher
int nextSlashIndex = hostPortAndDistinguisher.IndexOf('/');
if (nextSlashIndex >= 0)
{
// host:port/distinguisher or host/distinguisher
hostAndPort = hostPortAndDistinguisher.Substring(0, nextSlashIndex); // Excludes Slash
distinguisher = hostPortAndDistinguisher.Substring(nextSlashIndex); // Includes Slash
host = hostAndPort; // We don't know if there is a port yet.
// No need to validate the distinguisher
}
// Check for port
int colonIndex = hostAndPort.LastIndexOf(':'); // Allow IPv6 addresses
if (colonIndex >= 0)
{
// host:port
host = hostAndPort.Substring(0, colonIndex); // Excludes colon
port = hostAndPort.Substring(colonIndex + 1); // Excludes colon
// Loosely validate the port just to make sure it was a port and not something else
UInt16 portValue;
if (!UInt16.TryParse(port, NumberStyles.Integer, CultureInfo.InvariantCulture, out portValue))
{
return inputServiceName;
}
// Re-include the colon for the final output. Do not change the port format.
port = hostAndPort.Substring(colonIndex);
}
hostType = Uri.CheckHostName(host); // Revaidate the host
}
if (hostType != UriHostNameType.Dns)
{
// UriHostNameType.IPv4, UriHostNameType.IPv6: Do not normalize IPv4/6 hosts.
// UriHostNameType.Basic: This is never returned by CheckHostName today
// UriHostNameType.Unknown: Nothing recognizable to normalize
// default Some new UriHostNameType?
return inputServiceName;
}
// Now we have a valid DNS host, normalize it.
Uri constructedUri;
// This shouldn't fail, but we need to avoid any unexpected exceptions on this code path.
if (!Uri.TryCreate(Uri.UriSchemeHttp + Uri.SchemeDelimiter + host, UriKind.Absolute, out constructedUri))
{
return inputServiceName;
}
string normalizedHost = constructedUri.GetComponents(
UriComponents.NormalizedHost, UriFormat.SafeUnescaped);
string normalizedServiceName = string.Format(CultureInfo.InvariantCulture,
"{0}{1}{2}{3}", prefix, normalizedHost, port, distinguisher);
// Don't return the new one unless we absolutely have to. It may have only changed casing.
if (Match(inputServiceName, normalizedServiceName))
{
return inputServiceName;
}
return normalizedServiceName;
}
// Assumes already normalized
internal static bool Match(string serviceName1, string serviceName2)
{
return (String.Compare(serviceName1, serviceName2, StringComparison.OrdinalIgnoreCase) == 0);
}
}
}
|