A HTTP file server in 130 lines of code

17 Apr 2010 in software-development | dotnet | web |

…of which 20 deal with mapping file extensions to MIME types.

For an integration test I want to be able to set up an HTTP server that serves files from a directory. While I was at it I ensured that a proper directory listing and links are provided by the web server so I can browse the contents of a directory from my browser. With the aid of the HttpListener this is quite easy to do.

The whole thing is wrapped inside a class HttpFileServer, implementing disposable.

public HttpFileServer(string rootPath)
{
  this.rootPath = rootPath;
  http = new HttpListener();
  http.Prefixes.Add("http://localhost:8889/");
  http.Start();
  http.BeginGetContext(requestWait, null);
}

public void Dispose()
{
  http.Stop();
}

Careful with the selection of your port. You’ll get an Exception if it is registered to another application, even if that application is not running. The requestWait method is a callback that allows you to react to incoming requests asynchronously. It should be implemented with the well known pattern introduced to .NEt almost a decade ago:

if (!http.IsListening) return;
var c = http.EndGetContext(ar);
http.BeginGetContext(requestWait, null);

c is of type HttpListenerContext. The callback will also be called when the HttpListener is stopped, hence make sure that you only do stuff when you really seem to have a legitimate request.

As long as you keep your method re-entrant (i.e. you do not try to keep any request-specific state in some instance variable) you should be fine with multiple requests leading to multiple threads handling those.

As a file server, I only need to differ between a file being requested, a directory or something I don’t know:

var url = tuneUrl(c.Request.RawUrl);
var fullPath = string.IsNullOrEmpty(url) ? rootPath : Path.Combine(rootPath, url);

if (Directory.Exists(fullPath))
  returnDirContents(c, fullPath);
else if (File.Exists(fullPath))
  returnFile(c, fullPath);
else 
  return404(c);

The dir contents just serves some HTML. Here, especially on a computer where German umlauts are inside directory and file names, you should not forget to put the proper encoding into the HTML header:

context.Response.ContentType = "text/html";
context.Response.ContentEncoding = Encoding.UTF8;
using (var sw = new StreamWriter(context.Response.OutputStream))
{
  sw.WriteLine("<html>");
  sw.WriteLine("<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head>");
  sw.WriteLine("<body><ul>");

When links contain spaces, umlauts etc. modern browsers should take care about escaping them correctly into a URL request sent to the server (at least Chrome does). On the server side you need to ensure that this is converted back into things understood by the classes in System.IO, which happily deal with UTF-8 encoded strings. Thankfully there is a class in .NET, the HttpUtility in System.Web that will be able to do that for you:

private static string tuneUrl(string url)
{
  url = url.Replace('/', '\\');
  url = HttpUtility.UrlDecode(url, Encoding.UTF8);
  url = url.Substring(1);
  return url;
}

When actually hitting a file, you need to read it and write it to the appropriate output stream. Just to be on the safe side regarding file size, I chose a chunked approach:

context.Response.ContentType = 
  getcontentType(Path.GetExtension(filePath));
using ( var fs = File.OpenRead(filePath))
{
  context.Response.ContentLength64 = fs.Length;
  int read;
  while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
    context.Response.OutputStream.Write(buffer, 0, read); 
}

Don’t forget in all cases to close the Output Stream. In the case of using a StreamWriter, it’s disposal will close the underlying stream, in the case of serving the file directly, you’ll need to do it yourself.

To make things a bit more comfortable for e.g. a browser, it is useful to provide an appropriate MIME type. This is pretty boring code and goes along the lines of this example. A fairly nice list can be found here:

private static string getcontentType(string extension)
{
  switch (extension)
  {
    case ".avi":  return "video/x-msvideo";
    case ".css":  return "text/css";
    ...

And for those who want to play with this, here’s the full monty: Shout it

Chronology

  |  
comments powered by Disqus