Writing a toy web server in C#
Thursday 10 December 2009 - Filed under Uncategorized
First, for those hardcore folks: We’re not going to be implementing the nitty-gritty details of HTTP today. I have done it once, in C, for an embedded system, and I have no great desire to repeat the experience. So we’ll be using HttpListener to do the heavy lifting for us. Blatant cheating, I know, but it will also allow us to cover some interesting behavior, rather than getting bogged down in minutiae.
First, let’s have an example of where we want to be going. To start serving stuff, all we should have to do is create a server object, specify what we want to serve, and start accepting connections:
class Program { static void Main(string[] args) { var server = new Server("http://localhost:6122/") { Routes = new [] { Server.ServeFolder( "/static/", "static/" ), Server.ServeError( "/", 404, "Not Found" ), } }; for (; ; ) server.AcceptOne(); } }
The Routes array maps URIs to handlers. The web server will dispatch to the first handler whose virtual path is a prefix of the URI. Note that we’re providing static methods on the Server class to encapsulate various common things we’d like to serve – a folder on disk, a specific error page, or whatever. There is a fair amount of closure-based plumbing behind the scenes to make this work, but we’ll get to that soon enough.
If this looks like it was inspired by the way you map things in those dynamic web development environments,… that’s because it was. I hate the old way of mapping behavior directly onto files on disk, which contain server-side scripts to implement the cool stuff. It makes sane URI design very difficult, and exposes needless implementation detail to the user. So let’s not do that. Routes express the mappings from virtual paths to handlers. That is all.
Let’s begin with a very lightweight Server class, which doesn’t implement any of those helpers, but does the core business of dispatching requests.
using Route = Pair<string, Action<HttpListenerContext>>; class Server { HttpListener listener = new HttpListener(); public Route[] Routes = { }; public Server( string rooturi ) { listener.Prefixes.Add(rooturi); listener.Start(); } public void AcceptOne() { var ctx = listener.GetContext(); try { var route = Routes.First( r => ctx.Request.RawUrl.StartsWith(r.First)); Console.Write("{0} {1}: ", ctx.Request.HttpMethod, ctx.Request.RawUrl); route.Second(ctx); Console.WriteLine("{0} {1}", ctx.Response.StatusCode, ctx.Response.StatusDescription); } catch (Exception e) { Console.WriteLine("Error."); Console.WriteLine(e); ctx.Close(); } } }
The Route type is just an alias for something horrific to type: a pair consisting of a URI prefix and a function that takes HttpListenerContext and returns nothing. If we don’t have this, things get rather unweildy once we start building routes.
In the constructor, we bind ourselves against a root URI, and start the HttpListener. This does a whole pile of stuff behind the scenes with HttpApi and ultimately, http.sys in the kernel. Why they did this in the kernel, I don’t know, but that’s the way Microsoft wanted it, and that’s the way it’s gonna be.
The other method accepts a single request from the listener. We block in listener.GetContext() until a request is available. If we wanted to be clever, there is an asynchronous version of GetContext(), but I don’t want to go there until we have to.
Then we pick the first route whose URI is a prefix of the request URI. If there is none, that’s going to throw, which we deal with later. Assuming everything went well, we log a bit of debug to the console, and dispatch to the handler.
If things didn’t go according to plan, we end up in the catch block, where we log a failure, log the exception details, and drop it on the floor. A more useful implementation would return a defined error (500, perhaps) in the unhappy case, but I’ll leave that for you.
Next, we’re going to need some means of creating routes. The simplest is a route that just serves a constant error. Here’s a simple implementation of Server.ServeError:
public static Route ServeError(string path, int errorCode, string content) { return new Route(path, ctx => { ctx.Response.StatusCode = errorCode; ctx.Response.StatusDescription = content; ctx.Response.OutputStream.Write(content); ctx.Response.Close(); }); }
This will be a familiar pattern for all the helpers. We capture everything that can’t be provided at request-time in the closure. In a more strictly OO language, that closure would manifest as an object instance – hopefully implementing an interface IRequestHandler or similar, but in reality more likely to be a subclass of some RequestHandler type that provides all manner of “common” services. The object-oriented and hybrid-OO-functional approaches are equivalent (up to verbosity, haha!).
Another common thing we’ll want to do is serve a folder full of stuff off the disk. This isn’t actually something you want to do in its simplest form, since it conflates on-disk layout with URI-space layout of resources. But still, it is useful to be able to serve stuff from files:
public static Route ServeFolder(string virtualPath, string physicalPath) { return new Route(virtualPath, ctx => { var phys = ctx.Request.RawUrl.Replace(virtualPath, physicalPath); if (!File.Exists(phys)) { ServeError("", 404, "Not Found").Second(ctx); return; } var contentType = GuessContentType(Path.GetExtension(phys)); ctx.Response.Headers[HttpResponseHeader.ContentType] = contentType; var content = File.ReadAllBytes(phys); ctx.Response.OutputStream.Write(content); ctx.Response.Close(); }); }
static string GuessContentType(string ext) { switch (ext) { case ".js": return "text/javascript"; case ".htm": case ".html": return "text/html"; case ".png": return "image/png"; case ".jpg": return "image/jpg"; case ".css": return "text/css"; default: return "application/octet-stream"; } }
public static void Write(this Stream s, byte[] data) { s.Write(data, 0, data.Length); }
We’re mapping a virtual path (prefix) onto a physical path in the filesystem. To keep things simple, we do that just by replacing the virtual path wherever it occurs in the request URI. For the real world, this is inadequate, for security reasons – this route will happily go serving things from anywhere else on the filesystem, provided the attacker can construct a suitable relative path. A better approach is to canonicalize the path, and then reject the request if it doesn’t fall within the route’s physicalPath.
Moving on, we check if the file exists, and use the previous helper ServeError to throw back a 404 if it doesn’t. Next, we need to guess the content type for the client. We could use the platform’s MIME mappings here, but for simplicity, we’ll just hardcode the mapping for the file types we’re most likely to use.
All that’s left to do is read the content of the file, and write it into the stream, closing it when we’re done.
That’s enough to make our example work. Let’s cook up a quick HTML file, dump it in a static/ folder, and try it out:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Toy Web Server Example</title> </head> <body> <h1>It Works!</h1> </body> </html>
Fire up your browser, point it to http://localhost:6122/static/index.html and you get the following:
That’s enough for now; there’s a lot of directions in which we can extend this, and I will cover some of them (including serving dynamic content nicely) in the next post, but I encourage you to explore what’s possible on your own.
2009-12-10 » admin