How to do a custom secured file access with Http Handler and MVC framework<!-- --> | <!-- -->Patrick Desjardins Blog
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to do a custom secured file access with Http Handler and MVC framework

Posted on: September 15, 2012

In diverse scenario you could require to give to your user files. You can let them access them directly by having a directory of files and give the complete path to the file but this can lead to different issue. First of all, this will bind the server folders/files structures to the url that you give the the user which might not be interesting. Second, it doesn't give you any control over the files shared. Third, you may want to add algorithm for caching or give expiry to some files. All those customs code can't be done by giving a direct access to files, the solution : using Http Handler.

HttpHandler was there in Asp.Net traditional and in Asp.Net MVC is even easier because not configuration is required for IIS.

First, you need to add a new route to let know the MVC routing system that the url sent is not going to go to a traditional controller but to go to your http handler.

1public class ExternalFileRouteHandler : IRouteHandler {
2 public IHttpHandler GetHttpHandler(RequestContext requestContext) { }
3}

Inside the Global.asax.cs you need to add your route. You need to put the new route BEFORE the default route because otherwise it won't trig the route. The reason is that because the default route has default parameter that 2 parameters will automatically goes inside the Default route. But, if you set the FilesRoute before, the condition will match and this one will be executed.

1public static void RegisterRoutes(RouteCollection routes) {
2 routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
3 routes.Add("FilesRoute", new Route("files/{uniqueidentifier}", new ExternalFileRouteHandler()));
4 routes.MapRoute( "Default",
5 // Route name
6 "{controller}/{action}/{id}",
7 // URL with parameters
8 new { controller = "Home", action = "Index", id = UrlParameter.Optional
9 }
10 // Parameter defaults
11 );
12}

At this moment, the code doesn't compile because our IRouteHandler doesn't return anything from the GetHttpHandler. This is why we need to create a class that will inherit of IHttpHandler.

1public class ExternalFileHandler : IHttpHandler {
2
3public void ProcessRequest(HttpContext context) { }
4
5public bool IsReusable { get; private set; } }
6
7public class ExternalFileRouteHandler : IRouteHandler {
8 public IHttpHandler GetHttpHandler(RequestContext requestContext) {
9 return new ExternalFileHandler();
10 }
11}

At this point, we have nothing to do anymore from the perspective of the url. Every call will look like this:

http://yourwebsite.com/files/yourIdenfierHere.abc

Now, you need to get the file but first let's get some information about the request like the request itself, the response where we will send back the image and some information about the server and what file is requested.

1public class ExternalFileHandler : IHttpHandler {
2 public void ProcessRequest(HttpContext context) {
3
4 var response = context.Response;
5 var request = context.Request;
6 var server = context.Server;
7 var uniqueFileIdentifier = RequestContext.RouteData.Values["uniqueidentifier"].ToString();
8 }
9
10 public bool IsReusable { get; private set; }
11}

If we debug we can see that if the request is sent with the good url that the http handler catch the request with the good value.

ExternalFileHandlerSimple 400x123

The next step is to check if the file really exist. In fact, we need to check if the unique identifier match a file. If it's not the case, in a scenario of a people trying to get some body else file, it should return a message.

filenotfound

1public void ProcessRequest(HttpContext context) {
2
3 var response = context.Response;
4 var request = context.Request;
5 var server = context.Server;
6 var uniqueFileIdentifier = request.RequestContext.RouteData.Values["uniqueidentifier"].ToString();
7
8 FileInformation file = GetFromPersistenceStorage(uniqueFileIdentifier);
9 if (file == null) {
10 response.Write("File not found");
11 }
12}

As you can see, the file came from GetFromPersistenceStorage method. This method in reality would go into the database and search the file from the unique file identifier. This field in the database should be indexed and unique.

The next step is to check if the file is really on the server. It might be stored correctly into the database but not available on the server.

1public void ProcessRequest(HttpContext context) {
2
3 var response = context.Response;
4 var request = context.Request;
5 var server = context.Server;
6 var uniqueFileIdentifier = request.RequestContext.RouteData.Values["uniqueidentifier"].ToString();
7
8 FileInformation file = GetFromPersistenceStorage(uniqueFileIdentifier);
9
10 //Validate the file exist in the persistence storage
11 if (file == null) {
12 response.Write("File not found"); return;
13 }
14
15 //Validate the the file exist on the server physically
16 string completeFilePath = Path.Combine(file.PathOnServer, file.FileName);
17 if(!File.Exists(completeFilePath)) {
18 response.Write("File not on the server");
19 return;
20 }
21
22 //Prepare to send the file
23 response.Clear();
24 response.ContentType = file.FileType;
25 response.TransmitFile(completeFilePath);
26 response.End();
27}

filenotfoundserver

Finally, if the file is present, we will receive its content.

messagefromfile

You can now secure the file. This can be done in many ways. First, I suppose that the database table that contain every files will have some kind of relation between the user and files. Let say that every files has the possibility to allow many users, that mean the you should have 3 tables : User, UsersFiles and Files. UsersFiles table contain the id of the User that can access the file and the id of the file.

So the FileInformation class that we have defined earlier would have a List that can access the file. A small modification will be required to get the current user from the session and to check if the file allow the user.

1public void ProcessRequest(HttpContext context) {
2
3 var response = context.Response;
4 var request = context.Request;
5 var server = context.Server;
6 var uniqueFileIdentifier = request.RequestContext.RouteData.Values["uniqueidentifier"].ToString();
7
8 FileInformation file = GetFromPersistenceStorage(uniqueFileIdentifier);
9
10 //Validate the file exist in the persistence storage
11 if (file == null) {
12 response.Write("File not found");
13 return;
14 }
15
16 //Validate if the current logged user can access the file
17 if (!file.UserIdAuthorized.Contains(SessionUserId)) {
18 response.Write("You are not authorized to see this file.");
19 return;
20 }
21
22 //Validate the the file exist on the server physically
23 string completeFilePath = Path.Combine(file.PathOnServer, file.FileName);
24 if(!File.Exists(completeFilePath)) {
25 response.Write("File not on the server");
26 return;
27 }
28
29 //Prepare to send the file
30 response.Clear();
31 response.ContentType = file.FileType;
32 response.TransmitFile(completeFilePath);
33 response.End();
34}
35
36public class FileInformation {
37 public string PathOnServer { get; set; }
38 public string FileName { get; set; }
39 public string FileType { get; set; }
40 public List<int> UserIdAuthorized { get; set; }
41}

And that's it. You can from here add any other type of validation or expiry or caching. The method GetFromPersistenceStorage() could store into the cache the content of the file and if the file is requested again would get it from the cache instead of the server. You could also add expiry for file that may be deleted later by setting a timespan on the FileInformation and having a generated DateTime inside the table that contain the file information. A simple validation inside ProcessRequest will do the job.

You are now limitless about what to do with files on your server.