//------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Net;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Permissions;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Security.Windows
{
using AppFunc = Func, Task>;
///
/// A middleware that performs Windows Authentication of the specified types.
///
public sealed class WindowsAuthMiddleware
{
private Func, AuthTypes> _authenticationDelegate;
private AuthTypes _authenticationScheme = AuthTypes.Negotiate | AuthTypes.Ntlm | AuthTypes.Digest;
private string _realm;
private PrefixCollection _prefixes;
private bool _unsafeConnectionNtlmAuthentication;
private Func, ExtendedProtectionPolicy> _extendedProtectionSelectorDelegate;
private ExtendedProtectionPolicy _extendedProtectionPolicy;
private ServiceNameStore _defaultServiceNames;
private Hashtable _disconnectResults; // ulong -> DisconnectAsyncResult
private object _internalLock;
internal Hashtable _uriPrefixes;
private DigestCache _digestCache;
private AppFunc _nextApp;
// TODO: Support proxy auth
// private bool _doProxyAuth;
///
///
///
///
public WindowsAuthMiddleware(AppFunc nextApp)
{
if (Logging.On)
{
Logging.Enter(Logging.HttpListener, this, "WindowsAuth", string.Empty);
}
_internalLock = new object();
_defaultServiceNames = new ServiceNameStore();
// default: no CBT checks on any platform (appcompat reasons); applies also to PolicyEnforcement
// config element
_extendedProtectionPolicy = new ExtendedProtectionPolicy(PolicyEnforcement.Never);
_uriPrefixes = new Hashtable();
_digestCache = new DigestCache();
_nextApp = nextApp;
if (Logging.On)
{
Logging.Exit(Logging.HttpListener, this, "WindowsAuth", string.Empty);
}
}
///
/// Dynamically select the type of authentication to apply per request.
///
public Func, AuthTypes> AuthenticationSchemeSelectorDelegate
{
get
{
return _authenticationDelegate;
}
set
{
_authenticationDelegate = value;
}
}
///
/// Dynamically select the type of extended protection to apply per request.
///
public Func, ExtendedProtectionPolicy> ExtendedProtectionSelectorDelegate
{
get
{
return _extendedProtectionSelectorDelegate;
}
set
{
if (value == null)
{
throw new ArgumentNullException();
}
if (!ExtendedProtectionPolicy.OSSupportsExtendedProtection)
{
throw new PlatformNotSupportedException(SR.GetString(SR.security_ExtendedProtection_NoOSSupport));
}
_extendedProtectionSelectorDelegate = value;
}
}
///
/// Specifies which types of Windows authentication are enabled.
///
public AuthTypes AuthenticationSchemes
{
get
{
return _authenticationScheme;
}
set
{
_authenticationScheme = value;
}
}
///
/// Configures extended protection.
///
public ExtendedProtectionPolicy ExtendedProtectionPolicy
{
get
{
return _extendedProtectionPolicy;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
if (!ExtendedProtectionPolicy.OSSupportsExtendedProtection && value.PolicyEnforcement == PolicyEnforcement.Always)
{
throw new PlatformNotSupportedException(SR.GetString(SR.security_ExtendedProtection_NoOSSupport));
}
if (value.CustomChannelBinding != null)
{
throw new ArgumentException(SR.GetString(SR.net_listener_cannot_set_custom_cbt), "CustomChannelBinding");
}
_extendedProtectionPolicy = value;
}
}
///
/// Configures the service names for extended protection.
///
public ServiceNameCollection DefaultServiceNames
{
get
{
return _defaultServiceNames.ServiceNames;
}
}
///
/// The Realm for use in digest authentication.
///
public string Realm
{
get
{
return _realm;
}
set
{
_realm = value;
}
}
///
/// Enables authenticated connection sharing with NTLM.
///
public bool UnsafeConnectionNtlmAuthentication
{
get
{
return _unsafeConnectionNtlmAuthentication;
}
set
{
if (_unsafeConnectionNtlmAuthentication == value)
{
return;
}
lock (DisconnectResults.SyncRoot)
{
if (_unsafeConnectionNtlmAuthentication == value)
{
return;
}
_unsafeConnectionNtlmAuthentication = value;
if (!value)
{
foreach (DisconnectAsyncResult result in DisconnectResults.Values)
{
result.AuthenticatedUser = null;
}
}
}
}
}
internal Hashtable DisconnectResults
{
get
{
if (_disconnectResults == null)
{
Interlocked.CompareExchange(ref _disconnectResults, Hashtable.Synchronized(new Hashtable()), null);
}
return _disconnectResults;
}
}
internal unsafe void AddPrefix(string uriPrefix)
{
if (Logging.On)
{
Logging.Enter(Logging.HttpListener, this, "AddPrefix", "uriPrefix:" + uriPrefix);
}
string registeredPrefix = null;
try
{
if (uriPrefix == null)
{
throw new ArgumentNullException("uriPrefix");
}
(new WebPermission(NetworkAccess.Accept, uriPrefix)).Demand();
GlobalLog.Print("HttpListener#" + ValidationHelper.HashString(this) + "::AddPrefix() uriPrefix:" + uriPrefix);
int i;
if (string.Compare(uriPrefix, 0, "http://", 0, 7, StringComparison.OrdinalIgnoreCase) == 0)
{
i = 7;
}
else if (string.Compare(uriPrefix, 0, "https://", 0, 8, StringComparison.OrdinalIgnoreCase) == 0)
{
i = 8;
}
else
{
throw new ArgumentException(SR.GetString(SR.net_listener_scheme), "uriPrefix");
}
bool inSquareBrakets = false;
int j = i;
while (j < uriPrefix.Length && uriPrefix[j] != '/' && (uriPrefix[j] != ':' || inSquareBrakets))
{
if (uriPrefix[j] == '[')
{
if (inSquareBrakets)
{
j = i;
break;
}
inSquareBrakets = true;
}
if (inSquareBrakets && uriPrefix[j] == ']')
{
inSquareBrakets = false;
}
j++;
}
if (i == j)
{
throw new ArgumentException(SR.GetString(SR.net_listener_host), "uriPrefix");
}
if (uriPrefix[uriPrefix.Length - 1] != '/')
{
throw new ArgumentException(SR.GetString(SR.net_listener_slash), "uriPrefix");
}
registeredPrefix = uriPrefix[j] == ':' ? String.Copy(uriPrefix) : uriPrefix.Substring(0, j) + (i == 7 ? ":80" : ":443") + uriPrefix.Substring(j);
fixed (char* pChar = registeredPrefix)
{
i = 0;
while (pChar[i] != ':')
{
pChar[i] = (char)CaseInsensitiveAscii.AsciiToLower[(byte)pChar[i]];
i++;
}
}
GlobalLog.Print("HttpListener#" + ValidationHelper.HashString(this) + "::AddPrefix() mapped uriPrefix:" + uriPrefix + " to registeredPrefix:" + registeredPrefix);
_uriPrefixes[uriPrefix] = registeredPrefix;
_defaultServiceNames.Add(uriPrefix);
}
catch (Exception exception)
{
if (Logging.On)
{
Logging.Exception(Logging.HttpListener, this, "AddPrefix", exception);
}
throw;
}
finally
{
if (Logging.On)
{
Logging.Exit(Logging.HttpListener, this, "AddPrefix", "prefix:" + registeredPrefix);
}
}
}
internal PrefixCollection Prefixes
{
get
{
if (Logging.On)
{
Logging.Enter(Logging.HttpListener, this, "Prefixes_get", string.Empty);
}
if (_prefixes == null)
{
_prefixes = new PrefixCollection(this);
}
return _prefixes;
}
}
internal bool RemovePrefix(string uriPrefix)
{
if (Logging.On)
{
Logging.Enter(Logging.HttpListener, this, "RemovePrefix", "uriPrefix:" + uriPrefix);
}
try
{
GlobalLog.Print("HttpListener#" + ValidationHelper.HashString(this) + "::RemovePrefix() uriPrefix:" + uriPrefix);
if (uriPrefix == null)
{
throw new ArgumentNullException("uriPrefix");
}
if (!_uriPrefixes.Contains(uriPrefix))
{
return false;
}
_uriPrefixes.Remove(uriPrefix);
_defaultServiceNames.Remove(uriPrefix);
}
catch (Exception exception)
{
if (Logging.On)
{
Logging.Exception(Logging.HttpListener, this, "RemovePrefix", exception);
}
throw;
}
finally
{
if (Logging.On)
{
Logging.Exit(Logging.HttpListener, this, "RemovePrefix", "uriPrefix:" + uriPrefix);
}
}
return true;
}
internal void RemoveAll(bool clear)
{
if (Logging.On)
{
Logging.Enter(Logging.HttpListener, this, "RemoveAll", string.Empty);
}
try
{
// go through the uri list and unregister for each one of them
if (_uriPrefixes.Count > 0)
{
if (clear)
{
_uriPrefixes.Clear();
_defaultServiceNames.Clear();
}
}
}
finally
{
if (Logging.On)
{
Logging.Exit(Logging.HttpListener, this, "RemoveAll", string.Empty);
}
}
}
// old API, now private, and helper methods
private void Dispose(bool disposing)
{
GlobalLog.Assert(disposing, "Dispose(bool) does nothing if called from the finalizer.");
if (!disposing)
{
return;
}
try
{
_digestCache.Dispose();
}
finally
{
if (Logging.On)
{
Logging.Exit(Logging.HttpListener, this, "Dispose", string.Empty);
}
}
}
///
///
///
///
///
public Task Invoke(IDictionary env)
{
// Process the auth header, if any
if (!TryHandleAuthentication(env))
{
// If failed and a 400/401/500 was sent.
return Task.FromResult