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:
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:
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.
Here's an example of an Angular app (Jef King's Function Library) running in Azure Functions:
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!
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:
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:
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.
Here's an example of an Angular app (Jef King's Function Library) running in Azure Functions:
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!