2016-08-03 10:59:49 +00:00
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
#pragma warning disable 1634 , 1691
namespace System.ServiceModel.Dispatcher
{
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Diagnostics ;
using System.Diagnostics.CodeAnalysis ;
using System.Runtime ;
using System.ServiceModel ;
using System.ServiceModel.Activation ;
using System.ServiceModel.Channels ;
using System.ServiceModel.Description ;
using System.ServiceModel.Diagnostics ;
using System.ServiceModel.Web ;
using System.Net ;
public class WebHttpDispatchOperationSelector : IDispatchOperationSelector
{
public const string HttpOperationSelectorUriMatchedPropertyName = "UriMatched" ;
internal const string HttpOperationSelectorDataPropertyName = "HttpOperationSelectorData" ;
//
public const string HttpOperationNamePropertyName = "HttpOperationName" ;
internal const string redirectOperationName = "" ; // always unhandled invoker
internal const string RedirectPropertyName = "WebHttpRedirect" ;
string catchAllOperationName = "" ; // user UT=* Method=* operation, else unhandled invoker
Dictionary < string , UriTemplateTable > methodSpecificTables ; // indexed by the http method name
UriTemplateTable wildcardTable ; // this is one of the methodSpecificTables, special-cased for faster access
Dictionary < string , UriTemplate > templates ;
UriTemplateTable helpUriTable ;
public WebHttpDispatchOperationSelector ( ServiceEndpoint endpoint )
{
if ( endpoint = = null )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperArgumentNull ( "endpoint" ) ;
}
if ( endpoint . Address = = null )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperError ( new InvalidOperationException (
SR2 . GetString ( SR2 . EndpointAddressCannotBeNull ) ) ) ;
}
2017-08-21 15:34:15 +00:00
#pragma warning disable 56506 // Microsoft, endpoint.Address.Uri is never null
2016-08-03 10:59:49 +00:00
Uri baseUri = endpoint . Address . Uri ;
this . methodSpecificTables = new Dictionary < string , UriTemplateTable > ( ) ;
this . templates = new Dictionary < string , UriTemplate > ( ) ;
#pragma warning restore 56506
WebHttpBehavior webHttpBehavior = endpoint . Behaviors . Find < WebHttpBehavior > ( ) ;
if ( webHttpBehavior ! = null & & webHttpBehavior . HelpEnabled )
{
this . helpUriTable = new UriTemplateTable ( endpoint . ListenUri , HelpPage . GetOperationTemplatePairs ( ) ) ;
}
Dictionary < WCFKey , string > alreadyHaves = new Dictionary < WCFKey , string > ( ) ;
2017-08-21 15:34:15 +00:00
#pragma warning disable 56506 // Microsoft, endpoint.Contract is never null
2016-08-03 10:59:49 +00:00
foreach ( OperationDescription od in endpoint . Contract . Operations )
#pragma warning restore 56506
{
// ignore callback operations
if ( od . Messages [ 0 ] . Direction = = MessageDirection . Input )
{
string method = WebHttpBehavior . GetWebMethod ( od ) ;
string path = UriTemplateClientFormatter . GetUTStringOrDefault ( od ) ;
//
if ( UriTemplateHelpers . IsWildcardPath ( path ) & & ( method = = WebHttpBehavior . WildcardMethod ) )
{
if ( this . catchAllOperationName ! = "" )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperError (
new InvalidOperationException (
SR2 . GetString ( SR2 . MultipleOperationsInContractWithPathMethod ,
endpoint . Contract . Name , path , method ) ) ) ;
}
this . catchAllOperationName = od . Name ;
}
UriTemplate ut = new UriTemplate ( path ) ;
WCFKey wcfKey = new WCFKey ( ut , method ) ;
if ( alreadyHaves . ContainsKey ( wcfKey ) )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperError (
new InvalidOperationException (
SR2 . GetString ( SR2 . MultipleOperationsInContractWithPathMethod ,
endpoint . Contract . Name , path , method ) ) ) ;
}
alreadyHaves . Add ( wcfKey , od . Name ) ;
UriTemplateTable methodSpecificTable ;
if ( ! methodSpecificTables . TryGetValue ( method , out methodSpecificTable ) )
{
methodSpecificTable = new UriTemplateTable ( baseUri ) ;
methodSpecificTables . Add ( method , methodSpecificTable ) ;
}
methodSpecificTable . KeyValuePairs . Add ( new KeyValuePair < UriTemplate , object > ( ut , od . Name ) ) ;
this . templates . Add ( od . Name , ut ) ;
}
}
if ( this . methodSpecificTables . Count = = 0 )
{
this . methodSpecificTables = null ;
}
else
{
// freeze all the tables because they should not be modified after this point
foreach ( UriTemplateTable table in this . methodSpecificTables . Values )
{
table . MakeReadOnly ( true /* allowDuplicateEquivalentUriTemplates */ ) ;
}
if ( ! methodSpecificTables . TryGetValue ( WebHttpBehavior . WildcardMethod , out wildcardTable ) )
{
wildcardTable = null ;
}
}
}
protected WebHttpDispatchOperationSelector ( )
{
}
public virtual UriTemplate GetUriTemplate ( string operationName )
{
if ( operationName = = null )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperArgumentNull ( "operationName" ) ;
}
UriTemplate result ;
if ( ! this . templates . TryGetValue ( operationName , out result ) )
{
return null ;
}
else
{
return result ;
}
}
[SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#", Justification = "This method is defined by the IDispatchOperationSelector interface")]
public string SelectOperation ( ref Message message )
{
if ( message = = null )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperArgumentNull ( "message" ) ;
}
bool uriMatched ;
string result = this . SelectOperation ( ref message , out uriMatched ) ;
2017-08-21 15:34:15 +00:00
#pragma warning disable 56506 // Microsoft, Message.Properties is never null
2016-08-03 10:59:49 +00:00
message . Properties . Add ( HttpOperationSelectorUriMatchedPropertyName , uriMatched ) ;
#pragma warning restore 56506
if ( result ! = null )
{
message . Properties . Add ( HttpOperationNamePropertyName , result ) ;
if ( DiagnosticUtility . ShouldTraceInformation )
{
2017-08-21 15:34:15 +00:00
#pragma warning disable 56506 // Microsoft, Message.Headers is never null
2016-08-03 10:59:49 +00:00
TraceUtility . TraceEvent ( TraceEventType . Information , TraceCode . WebRequestMatchesOperation , SR2 . GetString ( SR2 . TraceCodeWebRequestMatchesOperation , message . Headers . To , result ) ) ;
#pragma warning restore 56506
}
}
return result ;
}
[SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#", Justification = "This method is like that defined by the IDispatchOperationSelector interface")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "This API needs to return multiple things")]
protected virtual string SelectOperation ( ref Message message , out bool uriMatched )
{
if ( message = = null )
{
throw DiagnosticUtility . ExceptionUtility . ThrowHelperArgumentNull ( "message" ) ;
}
uriMatched = false ;
if ( this . methodSpecificTables = = null )
{
return this . catchAllOperationName ;
}
2017-08-21 15:34:15 +00:00
#pragma warning disable 56506 // Microsoft, message.Properties is never null
2016-08-03 10:59:49 +00:00
if ( ! message . Properties . ContainsKey ( HttpRequestMessageProperty . Name ) )
{
return this . catchAllOperationName ;
}
HttpRequestMessageProperty prop = message . Properties [ HttpRequestMessageProperty . Name ] as HttpRequestMessageProperty ;
if ( prop = = null )
{
return this . catchAllOperationName ;
}
string method = prop . Method ;
Uri to = message . Headers . To ;
#pragma warning restore 56506
if ( to = = null )
{
return this . catchAllOperationName ;
}
if ( this . helpUriTable ! = null )
{
UriTemplateMatch match = this . helpUriTable . MatchSingle ( to ) ;
if ( match ! = null )
{
uriMatched = true ;
AddUriTemplateMatch ( match , prop , message ) ;
if ( method = = WebHttpBehavior . GET )
{
return HelpOperationInvoker . OperationName ;
}
message . Properties . Add ( WebHttpDispatchOperationSelector . HttpOperationSelectorDataPropertyName ,
new WebHttpDispatchOperationSelectorData ( ) { AllowedMethods = new List < string > ( ) { WebHttpBehavior . GET } } ) ;
return this . catchAllOperationName ;
}
}
UriTemplateTable methodSpecificTable ;
bool methodMatchesExactly = methodSpecificTables . TryGetValue ( method , out methodSpecificTable ) ;
if ( methodMatchesExactly )
{
string operationName ;
uriMatched = CanUriMatch ( methodSpecificTable , to , prop , message , out operationName ) ;
if ( uriMatched )
{
return operationName ;
}
}
if ( wildcardTable ! = null )
{
string operationName ;
uriMatched = CanUriMatch ( wildcardTable , to , prop , message , out operationName ) ;
if ( uriMatched )
{
return operationName ;
}
}
if ( ShouldRedirectToUriWithSlashAtTheEnd ( methodSpecificTable , message , to ) )
{
return redirectOperationName ;
}
// the {method, uri} pair does not match anything the service supports.
// we know at this point that we'll return some kind of error code, but we
// should go through all methods for the uri to see if any method is supported
// so that that information could be returned to the user as well
List < string > allowedMethods = null ;
foreach ( KeyValuePair < string , UriTemplateTable > pair in methodSpecificTables )
{
if ( pair . Key = = method | | pair . Key = = WebHttpBehavior . WildcardMethod )
{
// the uri must not match the uri template
continue ;
}
UriTemplateTable table = pair . Value ;
if ( table . MatchSingle ( to ) ! = null )
{
if ( allowedMethods = = null )
{
allowedMethods = new List < string > ( ) ;
}
//
if ( ! allowedMethods . Contains ( pair . Key ) )
{
allowedMethods . Add ( pair . Key ) ;
}
}
}
if ( allowedMethods ! = null )
{
uriMatched = true ;
message . Properties . Add ( WebHttpDispatchOperationSelector . HttpOperationSelectorDataPropertyName ,
new WebHttpDispatchOperationSelectorData ( ) { AllowedMethods = allowedMethods } ) ;
}
return catchAllOperationName ;
}
bool CanUriMatch ( UriTemplateTable methodSpecificTable , Uri to , HttpRequestMessageProperty prop , Message message , out string operationName )
{
operationName = null ;
UriTemplateMatch result = methodSpecificTable . MatchSingle ( to ) ;
if ( result ! = null )
{
operationName = result . Data as string ;
Fx . Assert ( operationName ! = null , "bad result" ) ;
AddUriTemplateMatch ( result , prop , message ) ;
return true ;
}
return false ;
}
void AddUriTemplateMatch ( UriTemplateMatch match , HttpRequestMessageProperty requestProp , Message message )
{
match . SetBaseUri ( match . BaseUri , requestProp ) ;
message . Properties . Add ( IncomingWebRequestContext . UriTemplateMatchResultsPropertyName , match ) ;
}
bool ShouldRedirectToUriWithSlashAtTheEnd ( UriTemplateTable methodSpecificTable , Message message , Uri to )
{
UriBuilder ub = new UriBuilder ( to ) ;
if ( ub . Path . EndsWith ( "/" , StringComparison . Ordinal ) )
{
return false ;
}
ub . Path = ub . Path + "/" ;
Uri originalPlusSlash = ub . Uri ;
bool result = false ;
if ( methodSpecificTable ! = null & & methodSpecificTable . MatchSingle ( originalPlusSlash ) ! = null )
{
// as an optimization, we check the table that matched the request's method
// first, as it is more probable that a hit happens there
result = true ;
}
else
{
// back-compat:
// we will redirect as long as there is any method
// - not necessary the one the user is looking for -
// that matches the uri with a slash at the end
foreach ( KeyValuePair < string , UriTemplateTable > pair in methodSpecificTables )
{
UriTemplateTable table = pair . Value ;
if ( table ! = methodSpecificTable & & table . MatchSingle ( originalPlusSlash ) ! = null )
{
result = true ;
break ;
}
}
}
if ( result )
{
string hostAndPort = GetAuthority ( message ) ;
originalPlusSlash = UriTemplate . RewriteUri ( ub . Uri , hostAndPort ) ;
message . Properties . Add ( RedirectPropertyName , originalPlusSlash ) ;
}
return result ;
}
static string GetAuthority ( Message message )
{
HttpRequestMessageProperty requestProperty ;
string hostName = null ;
if ( message . Properties . TryGetValue ( HttpRequestMessageProperty . Name , out requestProperty ) )
{
hostName = requestProperty . Headers [ HttpRequestHeader . Host ] ;
if ( ! string . IsNullOrEmpty ( hostName ) )
{
return hostName ;
}
}
IAspNetMessageProperty aspNetMessageProperty = AspNetEnvironment . Current . GetHostingProperty ( message ) ;
if ( aspNetMessageProperty ! = null )
{
hostName = aspNetMessageProperty . OriginalRequestUri . Authority ;
}
return hostName ;
}
// to enforce that no two ops have same UriTemplate & Method
class WCFKey
{
string method ;
UriTemplate uriTemplate ;
public WCFKey ( UriTemplate uriTemplate , string method )
{
this . uriTemplate = uriTemplate ;
this . method = method ;
}
public override bool Equals ( object obj )
{
WCFKey other = obj as WCFKey ;
if ( other = = null )
{
return false ;
}
return this . uriTemplate . IsEquivalentTo ( other . uriTemplate ) & & this . method = = other . method ;
}
public override int GetHashCode ( )
{
return UriTemplateEquivalenceComparer . Instance . GetHashCode ( this . uriTemplate ) ;
}
}
}
}