Pregunta

I would to intercept all post request to a custom WCF service (.net 3.5 SP1) in order to validate the presence of a specific header.

What I tried so far:

public class ServiceFactory : WebServiceHostFactory
{
    protected override System.ServiceModel.ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        var result = base.CreateServiceHost(serviceType, baseAddresses);

        result.Opened += result_Opened;

        return result;
    }

    private void result_Opened(object sender, EventArgs e)
    {
        var ctx = HttpContext.Current;
        var request = ctx.Request;
        if (request.HttpMethod == "POST")
        {              
            // Validate if the request contains my header
            if(request.Headers["MyHeader"] != "42")
                throw new VeryBadThingsException("boom");
        }
    }
}

I also set up my svc files to use this factory.

This is sometimes working. Actually, not all my web services calls are hooked by the open event handler. The web service actual implementation is reached, so I suppose the problem is not the web service itself.

What should I do to correctly hook all incoming requests to my service?

PS: to descbribe a bit more my context, the service is hosted by SharePoint 2010. That means I can't change the web.config file (technically it's possible, but it's a pain to deploy and maintain).

And I acutally inherits the class Microsoft.SharePoint.Client.Services.MultipleBaseAddressWebServiceHostFactory

¿Fue útil?

Solución 3

Ok, I manage to swim between all objects, with the help of the code project article Add Custom Message Header in WCF 4 Calls.

Especially, it helped me to figure out how to properly attach a ServiceBehavior through code, using attributes.

I finally have this:

internal class ValidateSPFormDigestAttribute : Attribute, IServiceBehavior
{
    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase host)
    {
        foreach (ChannelDispatcher cDispatcher in host.ChannelDispatchers)
        {
            foreach (EndpointDispatcher eDispatcher in cDispatcher.Endpoints)
            {
                eDispatcher.DispatchRuntime.MessageInspectors.Add(new ValidateSPFormDigestInspector());
            }
        }
    }

    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
    }
}

internal class ValidateSPFormDigestInspector : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {

        if (!SPUtility.ValidateFormDigest())
        {
            throw new FaultException(new FaultReason("Invalid form digest token"));
        }

        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
    }
}

And I attach my custom behavior on the service directly:

[BasicHttpBindingServiceMetadataExchangeEndpoint]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ValidateSPFormDigest]
public class MyCustomService: IWidgetAdminService

The immediate benefits, is that I no more require creating a custom web service factory!

Otros consejos

You should implement the IDispatchMessageInspector on the service side for this. The message instance passed to you in the AfterReceiveRequest method has a Headers property where you can check for your required headers.

Your current solution doesn't work for every call because it is only getting called when a new service host is opened. Once instantiated (and opened), that service host instance is servicing subsequent calls. But, because it is already opened, your code is not getting called on these subsequent calls.

You need to extend the WCF pipeline by adding a message inspector. The message inspector of the client will be responsible for adding the header and the message inspector of the server will be responsible for validating if the header exists.

Good practice: if you want to create custom headers, specify a custom namespace to ease lookup.

public static class WCFSOAPNamespaces
    {
        private const string root = "http://www.schemas.productname.com/";
        public const string Headers = root + "headers/";
    }

Server

The IDispatchMessageInspector handles all incoming messages to the server. This is the place where you will check for the existence of the header on the server.

public class DispatchMessageInspector : IDispatchMessageInspector
    {
        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            const string headerName = "nameOfTheHeader";

            var someHeaderData = request.Headers.GetHeader<string>(headerName, WCFSOAPNamespaces.Headers);
            //someHeaderData is the content that you want to check for every request. Attention: it throws System.ServiceModel.MessageHeaderException if the header doesn't exist   

            return null;
        }

        public void BeforeSendReply(ref Message reply, object correlationState) { }
    }

Client

The IClientMessageInspector handles messages on the client. If you need to add custom headers to the message, here is the place. If you do not need to add custom header, you can jump this first piece of code.

public class ClientMessageInspector : IClientMessageInspector
    {
        public void AfterReceiveReply(ref Message reply, object correlationState) { }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            const string headerName = "nameOfTheHeader";
            string headerContent = ""; //fill this variable with the content

            var header = new MessageHeader<string>(headerContent ?? string.Empty);
            var untyped = header.GetUntypedHeader(headerName, WCFSOAPNamespaces.Headers);
            request.Headers.Add(untyped);

            return null;
        }
    }

Both (client and server)

Even if you do not need a message inspector on the Client, you still need this configuration to add message inspection to your server-side application. More specifically, we need an EndpointBehavior to handle the MessageInspector. Then, we need to set the services endpoits to use this custom endpoint behavior.

In this example, I put the 2 inspectors in the same behavior, but you can create separate behaviors if you need.

   public class EndpointBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
        public EndpointBehavior() { }

        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new ClientMessageInspector());
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new DispatchMessageInspector());
        }

        public void Validate(ServiceEndpoint endpoint) { }

        public override Type BehaviorType
        {
            get { return this.GetType(); }
        }

        protected override object CreateBehavior()
        {
            return new EndpointBehavior();
        }
    }

Then, set your Endpoint to use this behavior.

Programmatically

...
ServiceEndpoint endpoint;
...
endpoint.Behaviors.Add(new EndpointBehavior());

Config

...
<services>
  <service name="...">
    <endpoint address="..." binding="..." contract="..." behaviorConfiguration="endpointBehaviorName" />
  </service>
...
<behaviors>
  ...
  <endpointBehaviors>
    <behavior name="endpointBehaviorName">
      <customEndpointBehavior />
    </behavior>
  </endpointBehaviors>
</behaviors>
...
<extensions>
  <behaviorExtensions>
    <add name="customEndpointBehavior" type="FullNamespace.EndpointBehavior , AssemblyName" />
  </behaviorExtensions>
</extensions>

From now on, all the requests will pass through this point. Hope it helps.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top