In-App Static Web Server with HttpListener in .Net

I was working on a Xamrin iOS application (using .Net) and one of the requirements was for the application to support a web view for presenting another form. The form would need to be served from within the application. There are lots of ways that one could accomplish this. For the requirements this only needed to be a static web server. The contents would be delivered via a zip file. Creating a static web server is pretty easy. I’ve created one before. Making this one would be easier.

What made this one so easy is that .Net provides the HttpListener class, which handles most of the socket/network related things for us. It will also parse out information from the incoming request and we can use it to generate a well formatted supply. It contains no logic for what replies should be sent for what circumstances, or for retrieving files from the file system, so on. That’s the part I had to build.

I was given an initial suggestion of getting the Zip file, using the .Net classes to decompress it and write it to the iPad’s file system, and retrieve the files from there. I started with that direction, but ended up with a different solution. Since the amount of data in the static website would be small, I thought it would be fine to leave it in the compressed archive. But if I changed my mind on this I wanted to be able to make adjustments with minimal effort.

Receiving Connections

To receive connections, the TcpListener class needs to know the prefix strings for requests. This prefix will usually contain http://localhost with a port number, such as http://localhost:8081/. It must end with the slash. Multiple prefixes can be specified. If you want the server to listen on all adapters for a specific port localhost could be replaced with * here. After creating a HttpListener these prefixes must be added to the listener’s Prefix collection.

String[] PrefixList
{
    get
    {
        return new string[] { "http://localhost:8081/",  "http://127.0.0.1:8081/", "http://192.168.1.242:8081/" };
    }
}

void ListenRoutine()
{
    _keepListening = true;
    listener = new HttpListener();
            
    foreach (var prefix in PrefixList)
    {
        listener.Prefixes.Add(prefix);
    }
            
    listener. Start();
    //...more code follows
}

The listener is ready to start listening for requests now. A call to TcpListener::GetContext() will block until a request comes in. Since it blocks, everything that I’m doing with the listener is on a secondary thread. I use the listener in a loop to keep replying to requests. The HttpListenerContext object contains an object representing the request (HttpListenerRequest) and the response (HttpListenerResponse). From the request, I am interested in the AbsolutePath of the request. This is the request URL Path with any query parameters removed. I’m also interested in the verb that was used on the request. For the server that I made I’m only handling GET requests.

while (_keepListening)
{
    //This call blocks until a request comes in
    HttpListenerContext context = listener.GetContext();
    HttpListenerRequest request = context.Request;
    HttpListenerResponse response = context. Response;


    ///Handle the request here

}
listener. Stop();

Let’s say that I wanted my server to return a hard coded response. I would need to know the size of that response in bytes. There is an OutputStream on the HttpListenerResponse object that I will write the entirety of my response to. Before I do, I set the ContentLength64 member of the HttpListenerResponse object.

async void HandleResponse(HttpListenerRequest request, HttpListenerResponse response)
{
    String responseString = "<html><body>Hello World</body></html>";
    byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(responseString);
    response.ContentLength64 = responseBytes.Length;
    var output = response.OutputStream;
    await output.WriteAsync(responseBytes, 0, responseBytes.Length);
    await output.FlushAsync();
    output. Close();
}

When I run the code now and navigate to the URL, I’ll see the text “Hello World” in the browser. But I want to be able to send more than just a hardcoded response. To make the server more useful it needs to send the property Mime Type header for certain content. I need to be able to easily change the content that it servers. To satisfy this goal I’ve externalized the data from the program and I’ve defined an interface to aid in adding new ways for the server to respond to the request. I’ll also want to be able to define other classes with different behaviours for requests. For those classes I’ve made the interface IRequestHandler. It defines two methods and two properties that the handlers must implement.

  • Prefix – this is a path prefix for the handler. It will only be considered as a class that can handle a response if the request’s absolute path starts with this prefix. If this field is an empty string then it can be considered for any request.
  • DefaultDocument – if no file name is specified in the path, then this is the document name that will be used.
  • CanHandleRequest(string method, string path) – This gives the class basic information on the request. If the class can handle the request it should return true from this method. If it returns false, it will no be given the request to process.
  • HandleRequest(HttpListenerRequest, HttpListenerResponse) – processes the actual request.

A list of these handlers will be made and added to a list. Each handler is considered for be given the request to handle one at a time until one is found that is appropriate for the request. When one is, it processes the request and no further handlers are considered. One of the handlers that I defined is the FileNotFoundHandler. It is the simplest of the request handlers. It can handle anything. Later, I’ll set this up as the last handler to be considered. If nothing else handles a request, thisn my FileNotFoundHandler will run.

public class FileNotFoundHandler : IRequestHandler
{
    public string Prefix => "/";

    public string DefaultDocument => "";

    public bool CanHandleRequest(string method, string path)
    {
        return true;
    }

    public async void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
    {
        String responseString = $"<html><body>Cannot find the file at the location [{request.Url.ToString()}]</body></html>";
        byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(responseString);
        response.StatusCode = 404;
        response.ContentLength64 = responseBytes.Length;
        var output = response.OutputStream;
        await output.WriteAsync(responseBytes, 0, responseBytes.Length);
        await output.FlushAsync();
        output. Close();
    }
}

Going back to the local server, I’m adding a list of IRequestHandler objects. The list will start with only the FileNotFoundHandler in it. Any other handlers added will be added at the front of the list, pushing everything back by one position. The last item added to the list will receive the highest priority.

List<IRequestHandler> _handlers = new List<IRequestHandler>();

public LocalServer(bool autoStart = false) {
    var fnf = new FileNotFoundHandler();
    AddHandler(fnf);
    if(autoStart)
    {
        Start();
    }
}

public void AddHandler(IRequestHandler handler)
{
    _handlers. Insert(0, handler);
}

void ListenRoutine()
{
    _keepListening = true;
    listener = new HttpListener();
            
    foreach (var prefix in PrefixList)
    {
        listener.Prefixes.Add(prefix);
    }
            
    listener. Start();
    while (_keepListening)
    {
        //This call blocks until a request comes in
        HttpListenerContext context = listener.GetContext();
        HttpListenerRequest request = context. Request;
        HttpListenerResponse response = context. Response;
        bool handled = false;
        foreach(var handler in _handlers)
        {
            if(handler.CanHandleRequest(request.HttpMethod, request.Url.AbsolutePath))
            {
                handler.HandleRequest(request, response);
                handled = true;
                break;
            }
        }
        if (!handled)
        {
            HandleResponse(request, response);
        }
    }
    listener. Stop();

}

This completes the functionality of the server itself, but I still need a handler. I mentioned earlier I wanted to serve content from a zip file. To do this I made a new handler named ZipRequestHandler. Some of the functionality that it will need will likely be part of almost any handler. I’ll put that functionality in a base class named RequestHandlerBase. This base class will define a DefaultDocument of index.html. It is also able to provide mime types based on a file extension. To retrieve mime types I have a string dictionary that maps an extension to a mimetype. Within the code I define some basic mime types. I don’t want all the mimetypes to be defined in source code. I have a JSON file that has a total of about 75 mime types in it. If that file were omitted for some reason the server would still have the foundational mime types provided here.

static StringDictionary ExtensionToMimeType = new StringDictionary();

static RequestHandlerBase()
{

            
    ExtensionToMimeType.Clear();
    ExtensionToMimeType.Add("js", "application/javascript");
    ExtensionToMimeType.Add("html", "text/html");
    ExtensionToMimeType.Add("htm", "text/html");
    ExtensionToMimeType.Add("png", "image/png");
    ExtensionToMimeType.Add("svg", "image/svg+xml");
    LoadMimeTypes();
}

        static void LoadMimeTypes()
        {
            try
            {
                var resourceStreamNameList = typeof(RequestHandlerBase).Assembly.GetManifestResourceNames();
                var nameList = new List<String>(resourceStreamNameList);
                var targetResource = nameList.Find(x => x.EndsWith(".mimetypes.json"));
                if (targetResource != null)
                {
                    DataContractJsonSerializer dcs = new DataContractJsonSerializer(typeof(LocalContentHttpServer.Handler.Data.MimeTypeInfo[]));
                    using (var resourceStream = typeof(RequestHandlerBase).Assembly.GetManifestResourceStream(targetResource))
                    {
                        var mtList = dcs.ReadObject(resourceStream) as MimeTypeInfo[];
                        foreach (var m in mtList)
                        {
                            ExtensionToMimeType[m.Extension.ToLower()] = m.MimeTypeString.ToLower();
                        }
                    }

                }
            } catch
            {

            }
        }

Getting a mime type is a simple dictionary entry lookup. We will see this used in the child class ZipRequestHandler.

public static string GetMimeTypeForExtension(string extension)
{
    extension= extension.ToLower();
    if(extension.Contains("."))
    {
        extension = extension.Substring( extension.LastIndexOf("."));
    }
    if(extension.StartsWith('.'))
        extension = extension.Substring(1);
    if(ExtensionToMimeType.ContainsKey(extension))
    {
        return ExtensionToMimeType[extension];
    }
    return null;
}

The ZipRequestHandler accepts either a path to an archive or a ZipArchive object along with a prefix for the requests. Optionally someone can set the caseSensitive parameter to disable the ZipRequestHandler‘s default behaviour of making request case sensitive. I’ve defined a decompress parameter too, but haven’t implemented it. When I do, this parameter will be used to decide if the ZipRequestHandler will completely decompress an archive before using it or keep the data compressed in the zip file. The two constructors are not substantially different. Let’s look at the one that accepts a string for the path to the zip file.

ZipArchive _zipArchive;
readonly bool _decompress ;
readonly bool _caseSensitive = true;
Dictionary<string, ZipArchiveEntry> _entryLookup = new Dictionary<string, ZipArchiveEntry>();

public ZipRequestHandler(String prefix, string pathToZipArchive, bool caseSensitive = true, bool decompress = false):base(prefix)
{
    FileStream fs = new FileStream(pathToZipArchive, FileMode.Open, FileAccess.Read);
    _zipArchive = new ZipArchive(fs);            
    this._decompress = decompress;
    this._caseSensitive = caseSensitive;
    foreach (var entry in _zipArchive.Entries)
    {
        var entryName = (_caseSensitive) ? entry.FullName : entry.FullName.ToLower();
        _entryLookup[entryName] = entry;
    }
}

public override bool CanHandleRequest(string method, string path)
{
    if (method != "GET") return false;
    return Contains(path);
}

Given the ZipArchive I collect the entries in the zip and their path. When request come in I’ll use this to jump straight to the relevant entry. The effect of the caseSensitive parameter can be seen here. If the class is intended to run case insensitive, then I convert file names to lower case. For later lookups, the search name specified will also be converted to lower case. Provided that a request is using the GET verb and requests a file that is contained within the archive this class will report that it can handle the request.

Ofcourse, the handling of the request is where the real work happens. A request may have query parameters appended to the end of it. We don’t want those for locating a file. Url.AbsolutePath will give the request path with the query parameters removed. If the URL path is for a folder, then we append the name of the default document to the path. we also remove any leading slashes so that the name matches the path within the ZipArchive. While I use TryGetValue on the dictionary to retrieve the ZipEntry, this should always succeed since there was an earlier check for the presence of the file through the CanHandleRequest call. We then get the mimeType for the file using the method RequestHandlerBase::GetMimeTypeForExtension. If a mimetype was found then the value for the header Content-Type is set.

The rest of the code looks similar to the code that was returning the hard coded responses. The ZipEntry abstracts away the details of getting a file out of a ZipArchive so nicely that it looks like reading from any other stream. The file is read and sent to the requester.

public override void HandleRequest(HttpListenerRequest request, HttpListenerResponse response)
{
    var path = request.Url.AbsolutePath;

    if (path.EndsWith("/"))
        path += DefaultDocument;
    if (path.StartsWith("/"))
        path = path.Substring(1);

    if (_entryLookup.TryGetValue(path, out var entry))
    {
        var mimeType = GetMimeTypeForExtension(path);
        if(mimeType != null)
        {
            response.AppendHeader("Content-Type", mimeType);
        }
        try
        {
            var size = entry.Length;
            byte[] buffer = new byte[size];
            var entryFile = entry.Open();
            entryFile.Read(buffer, 0, buffer.Length);

            var output = response.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Flush();
            output.Close();
        }catch(Exception exc)
        {

        }
    }
    else
    {
                
    }
}

The code in its present state meets most of the current needs. I won’t be sharing the final version of the code here. That will be in a private archive. But I can share a version that is functional. You can find the source code on GitHub at the following address.

https://github.com/j2inet/LocalStaticWeb.Net


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Twitter: @j2inet

One thought on “In-App Static Web Server with HttpListener in .Net

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.