Anthony Chu Contact Me

Serving Static Files from Azure Functions

Thursday, March 9, 2017

I've written about how to serve a single HTML page or a single Swagger file with Azure Functions before. But it hasn't really been easy or even possible to serve an entire site with Azure Functions. With the release of a new feature called Azure Functions Proxies a couple of weeks ago, we can now create a pretty capable HTTP static file server using Azure Functions.

Static file server function

The first thing we want to do is create an Azure Function App with a function that acts as a file server. It's a typical C# function with an HTTP trigger:

using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.IO;
using MimeTypes;

const string staticFilesFolder = "www";
static string defaultPage = 
    string.IsNullOrEmpty(GetEnvironmentVariable("DEFAULT_PAGE")) ? 
    "index.html" : GetEnvironmentVariable("DEFAULT_PAGE");

public static HttpResponseMessage Run(HttpRequestMessage req, TraceWriter log)
{
    try
    {
        var filePath = GetFilePath(req, log);

        var response = new HttpResponseMessage(HttpStatusCode.OK);
        var stream = new FileStream(filePath, FileMode.Open);
        response.Content = new StreamContent(stream);
        response.Content.Headers.ContentType = 
            new MediaTypeHeaderValue(GetMimeType(filePath));
        return response;
    }
    catch
    {
        return new HttpResponseMessage(HttpStatusCode.NotFound);
    }
}

private static string GetScriptPath()
    => Path.Combine(GetEnvironmentVariable("HOME"), @"site\wwwroot");

private static string GetEnvironmentVariable(string name)
    => System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process);

The core of the function is pretty straight forward. We take in a file path in the query string, and we stream back the file from the server. The function serves files out of a folder named www.

Here's the method that extracts the file parameter from the query string and builds the full path of the file on the server:

private static string GetFilePath(HttpRequestMessage req, TraceWriter log)
{
    var pathValue = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "file", true) == 0)
        .Value;

    var path = pathValue ?? "";

    var staticFilesPath = 
        Path.GetFullPath(Path.Combine(GetScriptPath(), staticFilesFolder));
    var fullPath = Path.GetFullPath(Path.Combine(staticFilesPath, path));

    if (!IsInDirectory(staticFilesPath, fullPath))
    {
        throw new ArgumentException("Invalid path");
    }

    var isDirectory = Directory.Exists(fullPath);
    if (isDirectory)
    {
        fullPath = Path.Combine(fullPath, defaultPage);
    }

    return fullPath;
}

If the supplied path is actually a directory, we'll return a path to the default page so we can attempt to serve it (by default, it is index.html);

Validating the path

Because there's a possibility of malicious input, such as an attempt to serve a file outside of www, we have a method to check that the path is within www:

private static bool IsInDirectory(string parentPath, string childPath)
{
    var parent = new DirectoryInfo(parentPath);
    var child = new DirectoryInfo(childPath);

    var dir = child;
    do
    {
        if (dir.FullName == parent.FullName)
        {
            return true;
        }
        dir = dir.Parent;
    } while (dir != null);

    return false;
}

Detecting MIME Types

When serving a file, we have to send the correct Content-type header in the response. To do this, we'll use a NuGet package called MediaTypeMap to detect the MIME type:

private static string GetMimeType(string filePath)
{
    var fileInfo = new FileInfo(filePath);
    return MimeTypeMap.GetMimeType(fileInfo.Extension);
}

Azure Functions Proxies

Our file server function is called like this:

https://myapp.azurewebsites.net/api/StaticFileServer?file=images/foo.png

What we really want is for it to look more like this:

https://myapp.azurewebsites.net/images/foo.png

This is where the new Proxies feature comes in. We can create a proxy that maps a desired URL to a URL that invokes the function.

Before we can use proxies, though, we need to turn the feature on:

Enable Proxies

When enabled, an app setting named ROUTING_EXTENSION_VERSION is added.

We can now create a proxy that will forward all traffic to our function:

Proxies Settings

This generates a proxies.json file in the app root that looks like this:

{
    "proxies": {
        "files": {
            "matchCondition": {
                "route": "{*path}"
            },
            "backendUri": "https://%WEBSITE_SITE_NAME%.azurewebsites.net/api/StaticFileServer?file={path}"
        }
    }
}

Instead of hardcoding the backend URI, we use a Kudu environment variable WEBSITE_SITE_NAME.

Trying it out

Now all we have to do is drop a few files into a folder named www in the app root.

www

Here's an example of an Angular app (Jef King's Function Library) running in Azure Functions:

it works

Source code

The full source code for this can be found at:

https://github.com/anthonychu/azure-functions-static-file-server

There is also a nifty "Deploy to Azure" button where you can deploy your own instance with a click of a button!

deploy