Allow IIS serving selected static files through ASP.NET pipeline - asp.net-mvc

In my project we change one image depending on domain which was requested. Of course we can serve different files (css, js, html), but much more easy is to handle this one file and serve user correct one (let's call it logo.ico)
In web.config we set <modules runAllManagedModulesForAllRequests="false"> which causes that that IIS serve all static files for us.
Is there a way to add exception for `logo.ico' file?
To make example clear: when browser request http://mydomian.com/logo.ico I would like to run custom handler. For every other file I would like to use IIS native solution (which means <modules runAllManagedModulesForAllRequests="false">)

There is no way to add exception just for one file, but you could serve that image with ashx handler, for example url should look like (add your own logic to select image)
http://mydomian.com/ServeImage.ashx?image=logo.ico
And then just serve icon for that domain:
public class ServeImage : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "image/x-icon";
using (var fs = new FileStream(context.Server.MapPath("~/App_Data/logo.ico"), FileMode.Open))
{
fs.CopyTo(context.Response.OutputStream);
}
}
public bool IsReusable
{
get
{
return false;
}
}
}

Related

.NET MVC DisplayModeProvider fallback

I am currently using DisplayModeProvider to check if a mobile request is coming in and serving up a Page.mobile.cshtml file if I detect a mobile request otherwise I'm serving the default page Page.cshtml. This also works as a fall-back - if there is a mobile request for PageX but PageX.mobile.cshtml does not exist but there is a PageX.cshtml, I serve PageX.cshtml. This is working as intended.
I would like to add to the fall-back behavior as I include support for tablet requests. So when a tablet device request is detected, if I have a Page.tablet.cshtml, it will go ahead and serve that file. If there isn't a ...tablet.cshtml file, I'd like it to try to serve the Page.mobile.cshtml file and if a Page.mobile.cshtml does not exist, we would serve the Page.cshtml file.
Is there a way to do this without having to create a ...tablet.csthml file for every page and Html.Partial'ing a ...mobile.cshtml within it?
You can do that by changing the route preference dynamically. Define the hierarchy as you want like tablet first then mobile and then web pages.
Here is a sample how CustomViewEngine can do that:
public class MyViewEngine : RazorViewEngine
{
public MyViewEngine()
: base()
{
ViewLocationFormats = new[] {
"~/Views/tab/{1}/%1/{0}.cshtml",
"~/Views/mobile/{1}/%1/{0}.cshtml",
"~/Views/{1}/%1/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new[] {
"~/Views/tab/%1/{1}/{0}.cshtml",
"~/Views/mobile/%1/{1}/{0}.cshtml",
"~/Views/%1/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
}
}
Here the view will be searched in the /Views/tab/ folder first then /Views/mobile/ followed by /Views/ and the /Views/Shared/ folders.
Detail of the implementation have been discussed here: ASP.NET MVC Custom View Routing

Performance ramifications of serving FilePath rather than View

What are the performance ramifications if any of serving a FilePathResult rather than a view (If we have created server cached copies of a website using a headless browser)
public class HomeController : Controller
{
public ActionResult Index()
{
var url = Request.RawUrl.Replace("/", "_");
var path = System.Configuration.ConfigurationManager.AppSettings["PreloadPath"] + "\\" + url + ".html";
if (System.IO.File.Exists(path))
{
return new FilePathResult(path, "text/html");
}
else
{
return View("Index");
}
}
}
We are having to access the AppSettings every request now, use the File System to check if a file exists, and then serve that html file.
What costs are there compared with just
return View("Index");
Will the file access have any cost on the server? Or am I talking nonsense, and IIS would have to perform some similar action?
Note: Please suggest any other tags if I should add them
Looking at the FilePathResult's source code you can see that in the end it goes down to WriteStreamAsText of HttpResponse. It's obvious that there is no magic call to IIS for example to handle the file directly without any .Net code taking place.
Having said that I still expect this to be somewhat faster than running a view, which possibly needs interpretation and execution.

Storing local files with ASP.NET Core and MVC

With Asp.NET Core, The handy path-finding functions in Environment are gone. HttpContext and HttpServerUtility have been stripped. And the Application store within the Cache framework is gone. I can no longer assume (in code) that my server is using IIS or that it's even running on a Windows box.
And I don't have a database; I have a set of JSON files. Which, for reasons outside the scope of this question, cannot be stored in a database.
How do I read and write to files on the server?
In the new ASP.NET Core world when we deploy we have 2 folders appRoot and wwwroot
we generally only put files below the wwwroot folder that we intend to serve directly with http requests. So if your json files are to be served directly ie consumed by client side js then maybe you would put them there, otherwise you would use a different folder below appRoot.
I will show below how to resolve paths for both scenarios, ie sample code how to save a json string to a folder below either appRoot or wwwroot. In both cases think of your location as a virtual path relative to one of those folders, ie /some/folder/path where the first / represents either appRoot or wwwroot
public class MyFileProcessor
{
public MyFileProcessor(IHostingEnvironment env, IApplicationEnvironment appEnv)
{
hostingEnvironment = env;
appEnvironment = appEnv;
appRootFolder = appEnv.ApplicationBasePath;
}
private IHostingEnvironment hostingEnvironment;
private IApplicationEnvironment appEnvironment;
private string appRootFolder;
public void SaveJsonToAppFolder(string appVirtualFolderPath, string fileName string jsonContent)
{
var pathToFile = appRootFolder + appVirtualFolderPath.Replace("/", Path.DirectorySeparatorChar.ToString())
+ fileName;
using (StreamWriter s = File.CreateText(pathToFile))
{
await s.WriteAsync(jsonContent);
}
}
public void SaveJsonToWwwFolder(string virtualFolderPath, string fileName string jsonContent)
{
var pathToFile = hostingEnvironment.MapPath(virtualFolderPath) + fileName;
using (StreamWriter s = File.CreateText(pathToFile))
{
await s.WriteAsync(jsonContent);
}
}
}

MachineKeyDataProtector - Invalid link when confirmation email sent through background job

I've been pulling my hair out over this. Anytime a user registration email is sent out via my windows service (background task), I get an "Invalid link".
My setup
I'm using Hangfire as a windows service on our development server. This is where the problematic GenerateEmailConfirmationToken call is happening. It's in a completely different context, outside of the ASP.NET pipeline. So I have setup machineKey values to correspond with that in the web.config of the MVC application:
In the app.config of the Windows Service Console project, which transforms to MyApp.exe.config, I have a machineKey element
In the MVC 5 project - I have a machineKey element that matches the MyApp.exe.config machineKey element.
I've verified that BOTH of these have the same machine key element data.
The Problem
When I generate a user using the ASP.NET MVC context and pipeline (IE without going through the Hangfire Background job processing), the link works fine.
When I use the background job processor, I always get invalid link. I'm all out of ideas here.
Why is this happening? Is it because the token is being generated in a different thread? How do I get around this?
Relevant code for the various projects
IoC Bootstrapping
Gets called by both applications (Windows Service and MVC Web App)
container.Register<IUserTokenProvider<AppUser, int>>(() => DataProtector.TokenProvider, defaultAppLifeStyle);
DataProtector.cs
public class DataProtector
{
public static IDataProtectionProvider DataProtectionProvider { get; set; }
public static DataProtectorTokenProvider<AppUser, int> TokenProvider { get; set; }
static DataProtector()
{
DataProtectionProvider = new MachineKeyProtectionProvider();
TokenProvider = new DataProtectorTokenProvider<AppUser, int>(DataProtectionProvider.Create("Confirmation", "ResetPassword"));
}
}
Things I've Tried
Using a DpapiDataProtectionProvider
Custom MachineKeyProtectionProvider from Generating reset password token does not work in Azure Website
The MachineKeyProtectionProvider.cs code is exactly as the linked post above.
I've also tried other purposes like "YourMom" and "AllYourTokensAreBelongToMe" to no avail. Single purposes, multiple purposes - it doesn't matter - none work.
I'm also calling HttpUtility.UrlEncode(code) on the code that gets generated in both places (Controller and Background Job).
Solution
igor got it right, except it was not a code issue. It was because of a rogue service picking up the job, which had a different machine key. I had been staring at the problem so long that I did not see a second service running.
As I understand your problem there are 2 possible places where failure could occur.
1. MachineKey
It could be that the MachineKey itself is not producing a consistent value between your 2 applications. This can happen if your machineKey in the .config file is not the same in both applications (I did read that you checked it but a simple type-o, added space, added to the wrong parent element, etc. could lead to this behavior.). This can be easily tested to rule it out as a point of failure. Also the behavior might be different depending on the referenced .net framework, MachineKey.Protect
The configuration settings that are required for the MachineKeyCompatibilityMode.Framework45 option are required for this method even if the MachineKeySection.CompatibilityMode property is not set to the Framework45 option.
I created a random key pair for testing and using this key I generated a test value I assigned to variable validValue below in the code. If you copy/paste the following section into your web.config and app.config the Unprotect of that keyvalue will work.
web.config / app.config
<system.web>
<httpRuntime targetFramework="4.6.1"/>
<machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209" validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>
Service Application Test
class Program
{
static void Main(string[] args)
{
// should evaluate to SomeTestString
const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
var unprotected2 = MachineWrapper.Unprotect(validValue);
}
}
Mvc Controller (or Web Api controller) Test
public class WebTestController : Controller
{
// GET: WebTest
public ActionResult Index()
{
// should evaluate to SomeTestString
const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
var unprotected2 = MachineWrapper.Unprotect(validValue);
return View(unprotected2);
}
}
Common Code
using System;
using System.Linq;
using System.Text;
using System.Web.Security;
namespace Common
{
public class MachineWrapper
{
public static string Protect()
{
var testData = "SomeTestString";
return BytesToString(MachineKey.Protect(System.Text.Encoding.UTF8.GetBytes(testData), "PasswordSafe"));
}
public static string Unprotect(string data)
{
var bytes = StringToBytes(data);
var result = MachineKey.Unprotect(bytes, "PasswordSafe");
return System.Text.Encoding.UTF8.GetString(result);
}
public static byte[] StringToBytes(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
public static string BytesToString(byte[] bytes)
{
var hex = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
hex.AppendFormat("{0:x2}", b);
return hex.ToString().ToUpper();
}
}
}
If this passes both Console and the Web Application will get the same value and not throw a CryptographicException message Error occurred during a cryptographic operation. If you want to test with your own keys just run Protect from the common MachineWrapper class and record the value and re-execute for both apps.
2. UserManager uses Wrong Type
I would start with the previous section BUT the other failure point is that your custom machine key provider is not being used by the Microsoft.AspNet.Identity.UserManager. So here are some questions/action items that can help you figure out why this is happening:
Is container.Register the Unity IoC framework or are you using another framework?
Are you sure that your Di framework is also injecting that instance in the Microsoft.AspNet.Identity.UserManager in both the Service application as well as the Web application?
Have put a break point in public byte[] Protect of your MachineKeyDataProtector class to see if this is called in both the Service application as well as the Web application?
From examples I have seen so far (including the one you posted with the custom MachineKey solution) you need to manually bootstrap the type during application startup but then again I have not ever tried to hook into the Identity framework to replace this component using DI.
If you look at the default Visual Studio template code that is provided when you create a new MVC application the code file App_Start\IdentityConfig.cs would be the place to add this new provider.
Method:
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
Replace
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
With this
var provider = new MachineKeyProtectionProvider();
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(provider.Create("ResetPasswordPurpose"));
And this has to be configured for both applications if you are not using a common library where this is configured.

Using MVC bundling to generate custom CSS from LESS files

I have a multiclient system whereby different clients have different brand colours.
These colours are stored in the DB and referenced throughout my LESS files as #color{1-3}.
We used to maintain a colors.less file with a reference to all places where these colours featured. In a client-neutral situation we would just render the normal bundle with our brand colours but within a client area we would inject a stored CSS file generated when the client colours changed in the DB.
This worked fine but maintaining the colours.less file was becoming a bit unwieldy so what I'd like to do is render the css "real time" so that no css file needs to be generated manually. Obviously I can't do this every request because it would hammer the server with a fairly intensive css generation every page load.
My question is how can i use bundling or some other form of caching to generate this css on the fly (i.e without actually storing the css files) without hammering the server?
you can implement a custom http handler:
public class CustomHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
// Return false in case your Managed Handler cannot be reused for another request.
// Usually this would be false in case you have some state information preserved per request.
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
var sb = new StringBuilder();
// read values from db and cache them with HttpContext.Current.Cache
sb.Append(".black : { color: 'black'; }");
context.Response.Clear();
context.Response.ContentType = "text/css";
context.Response.Write(sb.ToString());
}
#endregion
}
web.config:
<system.webServer>
<handlers>
<add name="ColorsHandler" verb="GET" path="colors.axd" type="WebApplication3.Infrastructure.CustomHandler, WebApplication3, Version=1.0.0.0, Culture=neutral" />
</handlers>
</system.webServer>
the only limitation is that you cannot reference this handler as MVC Bundle because MVC bundle can handle only static files. Thus you have to reference it as stylesheet:
<link rel="stylesheet" href="~/colors.axd" type="text/css">

Resources