Skip to main content

3 posts tagged with "wcf"

View All Tags

HTML to PDF using a WCF Service

TL; DR - "Talk is cheap. Show me the code."#

Some time ago I wrote a post which demonstrated how you could make PDFs from HTML using C# and wkhtmltopdf. To my lasting surprise this has been the most popular post I've written. I recently put together an ASP.NET WCF service which exposed this functionality which I thought might be worth sharing. The code can be found on GitHub here.

A little more detail#

I should say up front that I'm still a little ambivalent about how sensible an idea this is. Behind the scenes this WCF service is remotely firing up wkhtmltopdf using System.Diagnostics.Process. I feel a little wary about recommending this as a solution for a variety of not particularly defined reasons. However, I have to say I've found this pretty stable and reliable. Bottom line it seems to work and work consistently. But I though I should include a caveat emptor; there is probably a better approach than this available. Anyway...

There isn't actually a great deal to say about this WCF service. It should (hopefully) just do what it says on the tin. Putting it together didn't involve a great deal of work; essentially it takes the code from the initial blog post and just wraps it in a WCF service called PdfMaker. The service exposes 2 methods:

  1. GetPdf - given a supplied URL this method creates a PDF and then returns it as a Stream to the client
  2. GetPdfUrl - given a supplied URL this method creates a PDF and then returns the location of it to the client

Both of these methods also set a Location header in the response indicating the location of the created PDF.

That which binds us#

The service uses webHttpBinding. This is commonly employed when people want to expose a RESTful WCF service. The reason I've used this binding is I wanted a simple "in" when calling the service. I wanted to be able to call the service via AJAX as well as directly by browsing to the service and supplying a URL-encoded URL like this:

http://localhost:59002/PdfMaker.svc/GetPdf?url=http%3A%2F%2Fnews.ycombinator.com/You may wonder why I'm using http://news.ycombinator.com for the example above. I chose this as Hacker News is a very simple site; very few resources and a small page size. This means the service has less work to do when creating the PDF; it's a quick demo.

I should say that this service is arguably **not** completely RESTful as each GET operation behind the scenes attempts to create a new PDF (arguably a side-effect). These should probably be POST operations as they create a new resource each time they're hit. However, if they were I wouldn't be able to just enter a URL into a browser for testing and that's really useful. So tough, I shake my fist at the devotees of pure REST on this occasion. (If I should be attacked in the street shortly after this blog is posted then the police should be advised this is good line of inquiry...)

Good behaviour#

It's worth noting that automaticFormatSelectionEnabled set to true on the behaviour so that content negotiation is enabled. Obviously for the GetPdf action this is rather meaningless as it's a stream that's passed back. However, for the GetPdfUrl action the returned string can either be JSON or XML. The Fiddler screenshots below demonstrate this in action:

Test Harness#

As a final touch I added in a test harness in the form of Demo.aspx. If you browse to it you'll see a screen a little like this:

It's fairly self-explanatory as you can see. And here's an example of the output generated when pointing at Hacker News:

And that's it. If there was a need this service could be easily extended to leverage the various options that wkhtmltopdf makes available. Hope people find it useful.

WCF - moving from Config to Code, a simple WCF service harness (plus implementing your own Authorization)

Last time I wrote about WCF I was getting up and running with WCF Transport Windows authentication using NetTcpBinding in an Intranet environment. I ended up with a WCF service hosted in a Windows Service which did pretty much what the previous post name implies.

Since writing that I've taken things on a bit further and I thought it worth recording my approach whilst it's still fresh in my mind. There's 3 things I want to go over:

  1. I've moved away from the standard config driven WCF approach to a more "code-first" style
  2. I've established a basic Windows Service hosted WCF service / client harness which is useful if you're trying to get up and running with a WCF service quickly
  3. I've locked down the WCF authorization to a single Windows account through the use of my own ServiceAuthorizationManager

Moving from Config to Code#

So, originally I was doing what all the cool kids are doing and driving the configuration of my WCF service and all its clients through config files. And why not? I'm in good company.

Here's why not: it gets *very* verbose *very* quickly....

Okay - that's not the end of the world. My problem was that I had ~10 Windows Services and 3 Web applications that needed to call into my WCF Service. I didn't want to have to separately tweak 15 or so configs each time I wanted to make one standard change to WCF configuration settings. I wanted everything in one place.

Now there's newer (and probably hipper) ways of achieving this. Here's one possibility I happened upon on StackOverflow that looks perfectly fine.

Well I didn't use a hip new approach - no I went Old School with my old friend the appSettings file attribute. Remember that? It's just a simple way to have all your common appSettings configuration settings in a single file which can be linked to from as many other apps as you like. It's wonderful and I've been using it for a long time now. Unfortunately it's pretty basic in that it's only the appSettings section that can be shared out; no <system.serviceModel> or similar.

But that wasn't really a problem from my perspective. I realised that there were actually very few things that needed to be configurable for my WCF service. Really I wanted a basic WCF harness that could be initialised in code which implicitly set all the basic configuration with settings that worked (ie it was set up with defaults like maximum message size which were sufficiently sized). On top of that I would allow myself to configure just those things that I needed to through the use of my own custom WCF config settings in the shared appSettings.config file.

Once done I massively reduced the size of my configs from frankly gazillions of entries to just these appSettings.config entries which were shared across each of my WCF service clients and by my Windows Service harness:

<appSettings>
<add key="WcfBaseAddressForClient" value="net.tcp://localhost:9700/"/>
<add key="WcfWindowsSecurityApplied" value="true" />
<add key="WcfCredentialsUserName" value="myUserName" />
<add key="WcfCredentialsPassword" value="myPassword" />
<add key="WcfCredentialsDomain" value="myDomain" />
</appSettings>

And these config settings used only by my Windows Service harness:

<appSettings file="../Shared/AppSettings.config">
<add key="WcfBaseAddressForService" value="net.tcp://localhost:9700/"/>
</appSettings>

Show me your harness#

I ended up with a quite a nice basic "vanilla" framework that allowed me to quickly set up Windows Service hosted WCF services. The framework also provided me with a simple way to consume these WCF services with a minimum of code an configuration. No muss. No fuss. :-) So pleased with it was I that I thought I'd go through it here much in the manner of a chef baking a cake...

To start with I created myself a Windows Service in Visual Studio which I grandly called "WcfWindowsService". The main service class looked like this:

public class WcfWindowsService: ServiceBase
{
public static string WindowsServiceName = "WCF Windows Service";
public static string WindowsServiceDescription = "Windows service that hosts a WCF service.";
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public List<ServiceHost> _serviceHosts = null;
public WcfWindowsService()
{
ServiceName = WindowsServiceName;
}
public static void Main()
{
ServiceBase.Run(new WcfWindowsService());
}
/// <summary>
/// The Windows Service is starting
/// </summary>
/// <param name="args"></param>
protected override void OnStart(string[] args)
{
try
{
CloseAndClearServiceHosts();
//Make log4net startup
XmlConfigurator.Configure();
_logger.Warn("WCF Windows Service starting...");
_logger.Info("Global.WcfWindowsSecurityApplied = " + Global.WcfWindowsSecurityApplied.ToString().ToLower());
if (Global.WcfWindowsSecurityApplied)
{
_logger.Info("Global.WcfOnlyAuthorizedForWcfCredentials = " + Global.WcfOnlyAuthorizedForWcfCredentials.ToString().ToLower());
if (Global.WcfOnlyAuthorizedForWcfCredentials)
{
_logger.Info("Global.WcfCredentialsDomain = " + Global.WcfCredentialsDomain);
_logger.Info("Global.WcfCredentialsUserName = " + Global.WcfCredentialsUserName);
}
}
//Create binding
var wcfBinding = WcfHelper.CreateBinding(Global.WcfWindowsSecurityApplied);
// Create a servicehost and endpoints for each service and open each
_serviceHosts = new List<ServiceHost>();
_serviceHosts.Add(WcfServiceFactory<IHello>.CreateAndOpenServiceHost(typeof(HelloService), wcfBinding));
_serviceHosts.Add(WcfServiceFactory<IGoodbye>.CreateAndOpenServiceHost(typeof(GoodbyeService), wcfBinding));
_logger.Warn("WCF Windows Service started.");
}
catch (Exception exc)
{
_logger.Error("Problem starting up", exc);
throw exc;
}
}
/// <summary>
/// The Windows Service is stopping
/// </summary>
protected override void OnStop()
{
CloseAndClearServiceHosts();
_logger.Warn("WCF Windows Service stopped");
}
/// <summary>
/// Close and clear service hosts in list and clear it down
/// </summary>
private void CloseAndClearServiceHosts()
{
if (_serviceHosts != null)
{
foreach (var serviceHost in _serviceHosts)
{
CloseAndClearServiceHost(serviceHost);
}
_serviceHosts.Clear();
}
}
/// <summary>
/// Close and clear the passed service host
/// </summary>
/// <param name="serviceHost"></param>
private void CloseAndClearServiceHost(ServiceHost serviceHost)
{
if (serviceHost != null)
{
_logger.Info(string.Join(", ", serviceHost.BaseAddresses) + " is closing...");
serviceHost.Close();
_logger.Info(string.Join(", ", serviceHost.BaseAddresses) + " is closed");
}
}
}

As you've no doubt noticed this makes use of Log4Net for logging purposes (I'll assume you're aware of it). My Windows Service implements such fantastic WCF services as HelloService and GoodbyeService. Each revolutionary in their own little way. To give you a taste of the joie de vivre that these services exemplify take a look at this:

// Implement the IHello service contract in a service class.
public class HelloService : WcfServiceAuthorizationManager, IHello
{
// Implement the IHello methods.
public string GreetMe(string thePersonToGreet)
{
return "well hello there " + thePersonToGreet;
}
}

Exciting! WcfWindowsService also references another class called "Global" which is a helper class - to be honest not much more than a wrapper for my config settings. It looks like this:

static public class Global
{
#region Properties
// eg "net.tcp://localhost:9700/"
public static string WcfBaseAddressForService { get { return ConfigurationManager.AppSettings["WcfBaseAddressForService"]; } }
// eg true
public static bool WcfWindowsSecurityApplied { get { return bool.Parse(ConfigurationManager.AppSettings["WcfWindowsSecurityApplied"]); } }
// eg true
public static bool WcfOnlyAuthorizedForWcfCredentials { get { return bool.Parse(ConfigurationManager.AppSettings["WcfOnlyAuthorizedForWcfCredentials"]); } }
// eg "myDomain"
public static string WcfCredentialsDomain { get { return ConfigurationManager.AppSettings["WcfCredentialsDomain"]; } }
// eg "myUserName"
public static string WcfCredentialsUserName { get { return ConfigurationManager.AppSettings["WcfCredentialsUserName"]; } }
// eg "myPassword" - this should *never* be stored unencrypted and is only ever used by clients that are not already running with the approved Windows credentials
public static string WcfCredentialsPassword { get { return ConfigurationManager.AppSettings["WcfCredentialsPassword"]; } }
#endregion
}

WcfWindowsService creates and hosts a HelloService and a GoodbyeService when it starts up. It does this using my handy WcfServiceFactory:

public class WcfServiceFactory<TInterface>
{
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public static ServiceHost CreateAndOpenServiceHost(Type serviceType, NetTcpBinding wcfBinding)
{
var serviceHost = new ServiceHost(serviceType, new Uri(Global.WcfBaseAddressForService + ServiceHelper<TInterface>.GetServiceName()));
serviceHost.AddServiceEndpoint(typeof(TInterface), wcfBinding, "");
serviceHost.Authorization.ServiceAuthorizationManager = new WcfServiceAuthorizationManager(); // This allows us to control authorisation within WcfServiceAuthorizationManager
serviceHost.Open();
_logger.Info(string.Join(", ", serviceHost.BaseAddresses) + " is now listening.");
return serviceHost;
}
}

To do this it also uses my equally handy WcfHelper class:

static public class WcfHelper
{
/// <summary>
/// Create a NetTcpBinding
/// </summary>
/// <param name="useWindowsSecurity"></param>
/// <returns></returns>
public static NetTcpBinding CreateBinding(bool useWindowsSecurity)
{
var wcfBinding = new NetTcpBinding();
if (useWindowsSecurity)
{
wcfBinding.Security.Mode = SecurityMode.Transport;
wcfBinding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
}
else
wcfBinding.Security.Mode = SecurityMode.None;
wcfBinding.MaxBufferSize = int.MaxValue;
wcfBinding.MaxReceivedMessageSize = int.MaxValue;
wcfBinding.ReaderQuotas.MaxArrayLength = int.MaxValue;
wcfBinding.ReaderQuotas.MaxDepth = int.MaxValue;
wcfBinding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
wcfBinding.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
return wcfBinding;
}
}
/// <summary>
/// Create a WCF Client for use anywhere (be it Windows Service or ASP.Net web application)
/// nb Credential fields are optional and only likely to be needed by web applications
/// </summary>
/// <typeparam name="TInterface"></typeparam>
public class WcfClientFactory<TInterface>
{
public static TInterface CreateChannel(bool useWindowsSecurity, string wcfBaseAddress, string wcfCredentialsUserName = null, string wcfCredentialsPassword = null, string wcfCredentialsDomain = null)
{
//Create NetTcpBinding using universally
var wcfBinding = WcfHelper.CreateBinding(useWindowsSecurity);
//Get Service name from examining the ServiceNameAttribute decorating the interface
var serviceName = ServiceHelper<TInterface>.GetServiceName();
//Create the factory for creating your channel
var factory = new ChannelFactory<TInterface>(
wcfBinding,
new EndpointAddress(wcfBaseAddress + serviceName)
);
//if credentials have been supplied then use them
if (!string.IsNullOrEmpty(wcfCredentialsUserName))
{
factory.Credentials.Windows.ClientCredential = new System.Net.NetworkCredential(wcfCredentialsUserName, wcfCredentialsPassword, wcfCredentialsDomain);
}
//Create the channel
var channel = factory.CreateChannel();
return channel;
}
}

Now the above WcfHelper class and it's comrade-in-arms the WcfClientFactory don't live in the WcfWindowsService project with the other classes. No. They live in a separate project called the WcfWindowsServiceContracts project with their old mucker the ServiceHelper:

public class ServiceHelper<T>
{
public static string GetServiceName()
{
var customAttributes = typeof(T).GetCustomAttributes(false);
if (customAttributes.Length > 0)
{
foreach (var customAttribute in customAttributes)
{
if (customAttribute is ServiceNameAttribute)
{
return ((ServiceNameAttribute)customAttribute).ServiceName;
}
}
}
throw new ArgumentException("Interface is missing ServiceNameAttribute");
}
}
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class ServiceNameAttribute : System.Attribute
{
public ServiceNameAttribute(string serviceName)
{
this.ServiceName = serviceName;
}
public string ServiceName { get; set; }
}

Now can you guess what the WcfWindowsServiceContracts project might contain? Yes; contracts for your services (oh the excitement)! What might one of these contracts look like I hear you ask... Well, like this:

[ServiceContract()]
[ServiceName("HelloService")]
public interface IHello
{
[OperationContract]
string GreetMe(string thePersonToGreet);
}

The WcfWindowsServiceContracts project is included in *any* WCF client solution that wants to call your WCF services. It is also included in the WCF service solution. It facilitates the calling of services. What you're no doubt wondering is how this might be achieved. Well here's how, it uses our old friend the WcfClientFactory:

var helloClient = WcfClientFactory<IHello>
.CreateChannel(
useWindowsSecurity: Global.WcfWindowsSecurityApplied, // eg true
wcfBaseAddress: Global.WcfBaseAddressForClient, // eg "net.tcp://localhost:9700/"
wcfCredentialsUserName: Global.WcfCredentialsUserName, // eg "myUserName" - Optional parameter - only passed by web applications that need to impersonate the valid user
wcfCredentialsPassword: Global.WcfCredentialsPassword, // eg "myPassword" - Optional parameter - only passed by web applications that need to impersonate the valid user
wcfCredentialsDomain: Global.WcfCredentialsDomain // eg "myDomain" - Optional parameter - only passed by web applications that need to impersonate the valid user
);
var greeting = helloClient.GreetMe("John"); //"well hello there John"

See? Simple as simple. The eagle eyed amongst you will have noticed that client example above is using "Global" which is essentially a copy of the Global class mentioned above that is part of the WcfWindowsService project.

Locking down Authorization to a single Windows account#

I can tell you think i've forgotten something. "Tell me about this locking down to the single Windows account / what is this mysterious WcfServiceAuthorizationManager class that all your WCF services inherit from? Don't you fob me off now.... etc"

Well ensuring that only a single Windows account is authorised (yes dammit the original English spelling) to access our WCF services is achieved by implementing our own ServiceAuthorizationManager class. This implementation is used for authorisation by your ServiceHost and the logic sits in the overridden CheckAccessCore method. All of our WCF service classes will inherit from our ServiceAuthorizationManager class and so trigger the CheckAccessCore authorisation each time they are called.

As you can see from the code below, depending on our configuration, we lock down access to all our WCF services to a specific Windows account. This is far from the only approach that you might want to take to authorisation; it's simply the one that we've been using. However the power of being able to implement your own authorisation in the CheckAccessCore method allows you the flexibility to do pretty much anything you want:

public class WcfServiceAuthorizationManager : ServiceAuthorizationManager
{
protected static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
protected override bool CheckAccessCore(OperationContext operationContext)
{
if (Global.WcfWindowsSecurityApplied)
{
if ((operationContext.ServiceSecurityContext.IsAnonymous) ||
(operationContext.ServiceSecurityContext.PrimaryIdentity == null))
{
_logger.Error("WcfWindowsSecurityApplied = true but no credentials have been supplied");
return false;
}
if (Global.WcfOnlyAuthorizedForWcfCredentials)
{
if (operationContext.ServiceSecurityContext.PrimaryIdentity.Name.ToLower() == Global.WcfCredentialsDomain.ToLower() + "\\" + Global.WcfCredentialsUserName.ToLower())
{
_logger.Debug("WcfOnlyAuthorizedForWcfCredentials = true and the valid user (" + operationContext.ServiceSecurityContext.PrimaryIdentity.Name + ") has been supplied and access allowed");
return true;
}
else
{
_logger.Error("WcfOnlyAuthorizedForWcfCredentials = true and an invalid user (" + operationContext.ServiceSecurityContext.PrimaryIdentity.Name + ") has been supplied and access denied");
return false;
}
}
else
{
_logger.Debug("WcfOnlyAuthorizedForWcfCredentials = false, credentials were supplied (" + operationContext.ServiceSecurityContext.PrimaryIdentity.Name + ") so access allowed");
return true;
}
}
else
{
_logger.Info("WcfWindowsSecurityApplied = false so we are allowing unfettered access");
return true;
}
}
}

Phewwww... I know this has ended up as a bit of a brain dump but hopefully people will find it useful. At some point I'll try to put up the above solution on GitHub so people can grab it easily for themselves.

WCF Transport Windows authentication using NetTcpBinding in an Intranet environment

Update#

Since I wrote this initial post I've taken thinks on a bit further. Take a look at this post to see what I mean: http://icanmakethiswork.blogspot.com/2012/03/wcf-moving-from-config-to-code-simple.html I know I said I'd write about JSON this time. I will get to that but not this time. This time WCF authentication quirks. I've been working on a project that uses .NET Remoting to have a single central point to which web applications and Windows services can call into. This is used in an intranet environment and all the websites and Windows services were hosted on the same single server along with our .NET Remoting Windows service. (They could quite easily have been on different servers but there was no need in this case.) It was decided to "embrace the new" by migrating this .NET Remoting project over to WCF. The plan wasn't to do anything revolutionary, just to move from one approach to the other as easily as possible. I found the following useful article on MSDN: http://msdn.microsoft.com/en-us/library/aa730857%28v=vs.80%29.aspx This particular article was helpful and following the steps enclosed I was quickly up and running with a basic WCF service hosted in a Windows service. It was at this point I started thinking about security. The existing .NET Remoting approach had no security in place. This wasn't ideal but also probably wasn't the worry you might think. It was hosted in an intranet environment and hence not so exposed to the rigours of the Wild Wild Web. However, since I was looking at WCF I thought it would be a good opportunity to get some basic security in place. This generally pleases auditors. I opted to use Windows Transport authentication as this seemed pretty appropriate for an intranet environment. The idea being that we'd authenticate with Windows for an account in our domain. After headbutting Windows for some time I managed to get a successful client call going from the website running on my development machine to the (separate) development server that was hosting our WCF Window service using Transport Windows authentication. However, when deploying the website to the development server I discovered we would experience the following error when the website attempted to call the WCF service (on the same server). ``` Event Type: Failure Audit Event Source: Security Event Category: Logon/Logoff Event ID: 537 Date: 15/02/2012 Time: 16:32:04 User: NT AUTHORITY\SYSTEM Computer: MINE999 Description: Logon Failure: Reason: An error occurred during logon Logon Type: 3 Logon Process: ^ Authentication Package: NTLM Status code: 0xC000006D

Not terribly helpful. At the end of the day it seemed we were suffering from a security "feature" introduced by Microsoft to prevent services calling services on the same box with a fully qualified name. An explanation of this can be found here: [http://developers.de/blogs/damir\_dobric/archive/2009/08/28/authentication-problems-by-using-of-ntlm.aspx](<http://developers.de/blogs/damir_dobric/archive/2009/08/28/authentication-problems-by-using-of-ntlm.aspx>) Using method 1 in the enclosed link I initially worked round this by amending the registry and rebooting the server: [http://support.microsoft.com/kb/887993](<http://support.microsoft.com/kb/887993>) This was not a fantastic solution. Fortunately I subsequently found a better one but since the resources on the web are \***ATROCIOUS**\* on this point I thought I should take the time to note down the full explanation since otherwise it'll be lost in the mists of time. Here we go: The equivalent security to the previous .NET Remoting solution in WCF was to use this config setting on client and service: ```xml
<security mode="None" />

As I've said, this is an intranet environment and so having this "none" security setting in place is made less worrying by the fact that the network itself is secured. But obviously this is not ideal and unlikely to be audit compliant. To use Windows security you need this netTcpBinding config setting on client and service: ```xml

```

To call the service with this setting in place you will need to be an authenticated Windows user. (Or at the very least impersonating one - but you knew that.) NOW FOR THE MOST IMPORTANT BIT..... The endpoint addresses *must* be "localhost" for both

client and service when both are deployed to the same server. If this is not the case then you will suffer from the aforementioned security "feature" which will provide you with unhelpful "the server has rejected the client credentials" messages and *nothing* else. OK FINISHED - MOVE ALONG NOW... NOTHING MORE TO SEE HERE With WCF Windows Transport authentication in place you can interrogate the calling user id within the service methods by simply evaluating ServiceSecurityContext.Current.PrimaryIdentity.Name (which will be something like "myDomain\myUserName"). So we you wanted to, we could have a simple step which evaluated if the calling user is on the "approved" / "authorised" list. I'm sure this could be made more sophisticated by using groups etc I guess - though I haven't investigated it further as yet. In fact, I suspect Microsoft may have something even more sophisticated still available for use which I'm unaware of - if anyone knows a simple explanation of this then please do let me know! In closing, I do think Microsoft could work on providing more helpful error messages than "the server has rejected the client credentials". Going by what I read as I researched this error many people seem to have struggled much as I did before eventually bailing out and ended up chancing it by turning security off in their applications. Clearly it is not desirable to have people so confused by errors that they give up and settle for a less secure solution.