MVC 5 bundling and Azure CDN (query string) - asp.net-mvc

I have been following this tutorial: https://azure.microsoft.com/en-us/documentation/articles/cdn-serve-content-from-cdn-in-your-web-application/
Everything had been great until I noticed that bundled scripts and CSS files return with the cache: no-cache,expires: -1 and pragma: no-cache headers.
Of course, this has nothing to do with Azure. To prove this, I tested the bundles by accessing them directly from my site, instead of CDN - ie. mysite.com/bundles/mybundle?v={myassemblyversion}. The result was the same. When I disabled the CDN, and accessed the bundled file with the v query string generated by MVC, the headers were as expected: public caching, with the expiry time of one year.
I've tried to implement the IBundleTransform interface, but the context.BundleVirtualPath is read-only (even though it says gets or sets the virtual path...). I've also tried to modify the response headers at the Application_EndRequest(), but it didn't work, either. My last bet was writing IIS outbound rules, but since my bundles (used with "custom" v query string) don't return Last-Modified header, it was a futile attempt, too.
My question is: how can I use MVC bundling with Azure CDN if I want my bundled files to be cached on the client - that is, until the v query string changes?

I know I'm a little late to the game, but I did find a workaround. I'm making use of the code by Frison B Alexander here.
The issue is that once you rewrite the querystring for StyleBundles or ScriptBundles, the default caching behavior of one year resets to no-cache. This is solved by regenerating the exact same querystring per bundle that the MVC framework uses when specifying each Bundle's CDNPath.
Here's how it's done using the MVC Web App template. Here's the BundleConfig class:
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
//we have to go ahead and add our Bundles as if there is no CDN involved.
//this is because the bundle has to already exist in the BundleCollection
//in order to get the hash that the MVC framework will generate for the
//querystring.
Bundle jsBundle = new ScriptBundle("~/scripts/js3").Include(
"~/Scripts/jquery-{version}.js",
"~/Scripts/modernizr-*",
"~/Scripts/bootstrap.js",
"~/Scripts/respond.js");
bundles.Add(jsBundle);
Bundle cssBundle = new StyleBundle("~/content/css3").Include(
"~/Content/bootstrap.css",
"~/Content/site.css");
bundles.Add(cssBundle);
#if Debug
bundles.UseCdn = false;
#else
bundles.UseCdn = true;
//grab our base CDN hostname from web.config...
string cdnHost = ConfigurationManager.AppSettings["CDNHostName"];
//get the hashes that the MVC framework will use per bundle for
//the querystring.
string jsHash = GetBundleHash(bundles, "~/scripts/js3");
string cssHash = GetBundleHash(bundles, "~/content/css3");
//set up our querystring per bundle for the CDN path.
jsBundle.CdnPath = cdnHost + "/scripts/js3?v=" + jsHash;
cssBundle.CdnPath = cdnHost + "/content/css3?v=" + cssHash;
#endif
}
//Frison B Alexander's code:
private static string GetBundleHash(BundleCollection bundles, string bundlePath)
{
//Need the context to generate response
var bundleContext = new BundleContext(new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, bundlePath);
//Bundle class has the method we need to get a BundleResponse
Bundle bundle = BundleTable.Bundles.GetBundleFor(bundlePath);
var bundleResponse = bundle.GenerateBundleResponse(bundleContext);
//BundleResponse has the method we need to call, but its marked as
//internal and therefor is not available for public consumption.
//To bypass this, reflect on it and manually invoke the method
var bundleReflection = bundleResponse.GetType();
var method = bundleReflection.GetMethod("GetContentHashCode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
//contentHash is whats appended to your url (url?###-###...)
var contentHash = method.Invoke(bundleResponse, null);
return contentHash.ToString();
}
}

Related

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.

How to set the AntiForgeryToken cookie path

The former HtmlHelper.AntiForgeryToken method which allows one to override the string path is deprecated.
[ObsoleteAttribute("This method is deprecated. Use the AntiForgeryToken() method instead. To specify a custom domain for the generated cookie, use the <httpCookies> configuration element. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property.",
true)]
public MvcHtmlString AntiForgeryToken(
string salt,
string domain,
string path
)
Tells you to use <httpCookies>. BUT httpCookies Element does not have a setting for PATH.
Is this an oversight in the deprecation of this method? What is the best way to overwrite this cookie path? (manually?) Running website in a virtual application is not implicitly adding the application path to the __RequestVeririfcation cookie.
Looking at the deprecation message:
"This method is deprecated. Use the AntiForgeryToken() method instead. To specify a custom domain for the generated cookie, use the configuration element. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property."
It tells us we can validate additional parameters whenever the forgery token is read back. So even if we can't set the path in the cookie, we can set the path as a property inside the token. To validate it later on, for example:
public class AdditionalDataProvider : IAntiForgeryAdditionalDataProvider
{
public string GetAdditionalData(HttpContextBase context)
{
return AdditionalData(context);
}
public bool ValidateAdditionalData(HttpContextBase context, string additionalData)
{
var currentData = AdditionalData(context);
return currentData == additionalData;
}
private static string AdditionalData(HttpContextBase context)
{
var path = context.Request.ApplicationPath;
return path;
}
}
When asp.net generates the token it will store the current path (or any other unique value you want to validate) for that app and
if you have another app running on a different path, when the token gets sent to that app (due to the lack of cookie path) it will validate the previous app properties against that app's properties. If it is a different set of properties it will fail and deny the request.
Additionally, looking at the code for the AntiforgeryConfig.cs, if the app is running in a virtual directory, it will add that virtual directory in the cookie's name by default:
private static string GetAntiForgeryCookieName()
{
return GetAntiForgeryCookieName(HttpRuntime.AppDomainAppVirtualPath);
}
// If the app path is provided, we're generating a cookie name rather than a field name, and the cookie names should
// be unique so that a development server cookie and an IIS cookie - both running on localhost - don't stomp on
// each other.
internal static string GetAntiForgeryCookieName(string appPath)
{
if (String.IsNullOrEmpty(appPath) || appPath == "/")
{
return AntiForgeryTokenFieldName;
}
else
{
return AntiForgeryTokenFieldName + "_" + HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(appPath));
}
}
So it will be like this:
_RequestVerificationToken vs
_RequestVerificationToken_L2RIdjAz0
Meaning App2 although can receive tokens from App1, it won't be able to read them since it will be looking always for App2 verification token only.
HTH
For ASP.NET Core - See: AntiforgeryOptions Class
Cookie - Determines the settings used to create the antiforgery
cookies.
Ex (adapted from Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core):
services.AddAntiforgery(options =>
{
options.Cookie.Path = "Path";
});
The best aproach to overwrite AntiForgeryToken's cookie configuration (Path, HttpOnly,...) is with encapsulation (Microsoft team post).
It is possible to configure the cookie path instead of setting it on the properties.
public static class AntiForgeryTokenExtensions
{
///<summary>
///Generates a hidden form field (anti-forgery token) that is
///validated when the form is submitted. Furthermore, this extension
///applies custom settings on the generated cookie.
///</summary>
///<returns>Generated form field (anti-forgery token).</returns>
public static MvcHtmlString AntiForgeryTokenExtension(this HtmlHelper html)
{
// Call base AntiForgeryToken and save its output to return later.
var output = html.AntiForgeryToken();
// Check that cookie exists
if(HttpContext.Current.Response.Cookies.AllKeys.Contains(AntiForgeryConfig.CookieName))
{
// Set cookie into the variable
var antiForgeryTokenCookie = HttpContext.Current.Response.Cookies.Get(AntiForgeryConfig.CookieName);
// Set cookie configuration
antiForgeryTokenCookie.Path = "/Path";
// antiForgeryTokenCookie.HttpOnly = true;
// ...
}
return output;
}
}
There is a last change that must be done and it is replace AntiForgeryToken() for AntiForgeryTokenExtension() if it is an existing project.
NOTES
With this code you can configure AntiForgeryToken cookie as a normal cookie.
It is also possible to add input parameters to this method, but I am not sure it would be a good practice.
There are different ways to get the cookies but I think that through Response.Cookies is the "most correct", since it is a response cookie.
IMPORTANT
It is needed to check if cookie exist first before trying to get it. If you try to get a Response cookie which doesn't exist, it will be generated. It doesn't happen with Request cookies.
COOKIE KNOWLEDGE
It is not the question itself but explains part of the code and it is quite important to know when we are working with cookies, so I consider it is good to have this information here too.
All Response.Cookies are in Request.Cookies, but not all Request.Cookies are in Response.Cookies.
If you create a Response.Cookie it will appear also in Request.Cookies.
If you create a Request.Cookie it will NOT appear in Response.Cookies.
If you try to get a non-existent cookie from Request.Cookies it will return a null.
If you try to get a non-existent cookie Response.Cookies it will return a new generated cookie.
SOURCES
There is the link where the developers tell to use encapsulation and many other things that could be useful.
Microsoft developers recommendations and information
Source to knowledge of cookies, Request.Cookies and Response.Cookies differences.
Difference between request cookies and response cookies
Difference between request cookies and response cookies 2
Check if cookie exist and difference between kind of cookies

Owin/NancyFx Trailing Slash on Root Path

I have an Owin/NancyFx single-page application using AngularJs and UI Router.
Its hosted in IIS7 and for the most part everything is working. However there is one annoying issue with the root path that I can't seem to solve.
I would like a trailing slash on the root path, something like:
http://myserver.internaldomain.com/myapp/
This way when UI Router goes to handle the hashbang routing, all urls will look like:
http://myserver.internaldomain.com/myapp/#/mySpaRoute
However, I can't seem to get a trailing slash to append, so instead my URL looks like:
http://myserver.internaldomain.com/myapp#/mySpaRoute
I have tried to create an Owin middleware the looks at the URL and redirects if there's a missing / at the end. This works for all routes that are handled by the WebApi but not NancyFx. That seems reasonable since NancyFx takes over routing early to handle rendering its views.
Next I tried a NancyFx BeforeRequest pipeline lambda to do the same thing, interrogate the URL and append a / as needed. This however resulted in a redirect loop. The request would come in to the pipeline as: http://example.com/app, and then redirect to: http://example.com/app/, however at the next pipeline execution, the trailing / would be stripped and the pipeline handler would redirect again -- this is where the loop occured.
So I guess simply, how do I make NancyFx add a trailing / to the end of my routes?
Update:
Went to lunch, talked to the duck a bit, updated all the assemblies, then decided that its just the root get path that I really need to append the / to make hashbang routing look decent:
public class HomeModule : NancyModule
{
// note this works fine when running from localhost, but when running
// as an application in IIS, a redirect loop occurs
public HomeModule()
{
Get["/"] = _ =>
{
var requestUri = new Uri(Request.Url);
if (!requestUri.AbsoluteUri.EndsWith("/"))
{
var targetUri = requestUri.ToString() + "/";
return Response.AsRedirect(targetUri);
}
const string view = "views/home.cshtml";
var model = new { Title = Constants.ApplicationTitle };
return View[view, model];
}
}
}
Annnnnnd Redirect loop.
Ultimately this appears to have been caused by the Uri class. The Uri class does a very good job of removing trailing slashes in many cases. This means that I was, essentially, fixing any "malformed" urls by creating a new Uri out of them. Then I was breaking these nice Uri's by appending a / to them. On redirect the newly cast Uri would remove my extraneous /, then fail the if statement and the process would begin again, hence by redirect loop.
To fix the issue, I instead used the System.Web.HttpContextBase property provided in the owin environment context and checked the Request.Url property which seems to be the original requested Url with little or no post-processing.
These changes were made in my EnforceTrailingSlashMiddleware that I had written earlier. Here is the invoke method:
public override async Task Invoke(IOwinContext context)
{
var httpContext = context.Environment["System.Web.HttpContextBase"] as System.Web.HttpContextBase;
if (httpContext != null && httpContext.Request != null && httpContext.Request.Url != null)
{
var path = httpContext.Request.Url.ToString();
/*
formatter is a class ("SlashFormatter") with two methods:
"ShouldAppendSlash" which takes a path string and returns a boolean
(whether or not a slash should be appended)
"AppendSlash" which takes a string, safely appends a slash and
then returns the modified string.
*/
if (formatter.ShouldAppendSlash(path))
{
var url = formatter.AppendSlash(path);
context.Response.Redirect(url);
}
}
await Next.Invoke(context);
}

Resources