It seems a bit insane, but we need to serve up some simple static HTML/CSS/JS assets, and the alternative is bundling Apache httpd with our product, or requiring IIS, both of which are a bit unpalatable.
First, we need a ServiceContract which allows any path to go after our service’s root URL:
IUWebService.cs:
using System;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.IO;
namespace TaskService
{
[ServiceContract]
public interface IUWebService
{
[OperationContract]
[WebGet(UriTemplate = "*")]
Stream GetFile();
}
}
It’s important to set the UriTemplate to the wildcard, and to make our method have 0 arguments. This means it won’t try to process the URL and we can grab it verbatim for our own processing.
Now for the implementation,
UWebService.cs:
using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.ServiceModel.Description;
namespace TaskService
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, IncludeExceptionDetailInFaults = true)]
public class UWebService : IUWebService
{
public Stream GetFile()
{
Uri to = OperationContext.Current.IncomingMessageHeaders.To;
string path = to.AbsolutePath;
path = path.Substring("/UWebService.svc".Length);
string basepath = Utils.GetOurLocation();
DirectoryInfo di = new DirectoryInfo(basepath);
di = di.Parent;
di = di.Parent;
di = di.Parent;
// Get rid of leading slash
path = path.Substring(1);
// Replace / with \
path = path.Replace("/", "\\");
path = System.IO.Path.Combine(di.FullName,path);
// Set headers
// We have 'borrowed' the most important mime types from apache mime.types
string ext = new FileInfo(path).Extension;
string mimetype;
switch (ext)
{
case ".html":
case ".htm":
mimetype = "text/html";
break;
case ".js":
mimetype = "application/javascript";
break;
case ".css":
mimetype = "text/css";
break;
default:
mimetype = "text/plain";
break;
}
WebOperationContext.Current.OutgoingResponse.ContentType = mimetype;
WebOperationContext.Current.OutgoingResponse.ContentLength = new FileInfo(path).Length;
return System.IO.File.Open(path, FileMode.Open);
}
}
}
The key points here are getting the Uri of the incoming path, and then munging that path to get the part which is the requested file. In our product, we are interested in serving up files which are 3 directories above where the executable is running – hence the getParent() calls. The other notable things are setting the correct MIME type and content length.
Finally we open the file and return the stream. Currently there is no error checking!
For completeness, this is what Utils.GetOurLocation() does:
public static string GetOurLocation()
{
Uri thisLocation = new Uri(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase));
return thisLocation.LocalPath;
}
That’s not quite the complete story – we need a working Web.config and a discussion of why this won’t work in the ASP.NET development server in Visual Studio.
The Web.config:
<system.serviceModel>
<services>
<service name="TaskService.UWebService">
<endpoint address="" behaviorConfiguration="WebHttpBehavior" binding="webHttpBinding" contract="TaskService.IUWebService">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="WebHttpBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
<webHttpBinding>
<binding name="myHttpBinding" >
<security mode="None"/>
</binding>
</webHttpBinding>
</bindings>
</system.serviceModel>
This is the relevant system.serviceModel fragment. What caught me out is that you need the endpoint behavio[u]r as well as the binding to say that you want plain HTTP rather than XML/SOAP etc.
Now, we should be able to go to a URL, like
http://localhost:51060/UWebService.svc/gui/index.html
and be presented with our HTML UI. However, I found that any requests with dots (‘periods’) in them don’t work. It turns out there are some characters that WCF kindly filters out/chokes on if they are in the request. The grubby details are here, but if you scroll to the bottom, you will see that the problem only occurs if you are using the ASP.Net development server.
Well, we can live with this. I only need the ASP.Net development server to do some occasional debugging, and the rest of the time I use our custom self hosting solution, inspired by this idea, and it works perfectly there.
I didn’t mention this in the introduction, but another reason why this is so useful for me is I can now serve static content and dynamic SOAP/JSON services from the same host and port number. This means that the HTML UI can do clever AJAX/jQuery stuff without cross domain issues or proxying.
Wrapping up, this was easier than I thought, but some of the knowledge of the .NET/WCF stuff is hard-won and often only found in blogs, which was part of my motivation to write this up. Two main TODOs remain before this can be used “in the wild” which are of course security and error checking. Running under a limited service account or checking paths to restrict file system access (especially anything with “..”) are two approaches to ensure you don’t end up exposing the whole file system.