#Url.Content doesnt resolve absolute path on one server but does on another - asp.net

We currently have two different servers on same domain. But one server resolves
#Url.Content("~/api/User")'
as
http://domain.com/virtualdirectory/api/User
where as other server doesnt resolve it absolutely; rather it resolves it relatively like
api/user
The code base is same and we are using MVC4. I am not sure as to where we went wrong or if there is any IIS/DNS settings that need to be done in order to get this fixed.
All help is appreciated; thanks :)

This is related with the IIS Rewriting module in your IIS web server that return the path to http://domain.com/virtualdirectory/api/User
Take a look on the part of source code of #Url.Content below:
private static string GenerateClientUrlInternal(HttpContextBase httpContext, string contentPath)
{
if (String.IsNullOrEmpty(contentPath))
{
return contentPath;
}
// can't call VirtualPathUtility.IsAppRelative since it throws on some inputs
bool isAppRelative = contentPath[0] == '~';
if (isAppRelative)
{
string absoluteContentPath = VirtualPathUtility.ToAbsolute(contentPath, httpContext.Request.ApplicationPath);
return GenerateClientUrlInternal(httpContext, absoluteContentPath);
}
// we only want to manipulate the path if URL rewriting is active for this request, else we risk breaking the generated URL
bool wasRequestRewritten = _urlRewriterHelper.WasRequestRewritten(httpContext);
if (!wasRequestRewritten)
{
return contentPath;
}
// Since the rawUrl represents what the user sees in his browser, it is what we want to use as the base
// of our absolute paths. For example, consider mysite.example.com/foo, which is internally
// rewritten to content.example.com/mysite/foo. When we want to generate a link to ~/bar, we want to
// base it from / instead of /foo, otherwise the user ends up seeing mysite.example.com/foo/bar,
// which is incorrect.
string relativeUrlToDestination = MakeRelative(httpContext.Request.Path, contentPath);
string absoluteUrlToDestination = MakeAbsolute(httpContext.Request.RawUrl, relativeUrlToDestination);
return absoluteUrlToDestination;
}
Use the codes below to check whether your web servers are having the URL rewritten:
bool requestWasRewritten = (httpWorkerRequest != null && httpWorkerRequest.GetServerVariable("IIS_WasUrlRewritten") != null);
And Also:
private volatile bool _urlRewriterIsTurnedOnCalculated = false;
private bool _urlRewriterIsTurnedOnValue;
private object _lockObject = new object();
private bool IsUrlRewriterTurnedOn(HttpContextBase httpContext)
{
// Need to do double-check locking because a single instance of this class is shared in the entire app domain (see PathHelpers)
if (!_urlRewriterIsTurnedOnCalculated)
{
lock (_lockObject)
{
if (!_urlRewriterIsTurnedOnCalculated)
{
HttpWorkerRequest httpWorkerRequest = (HttpWorkerRequest)httpContext.GetService(typeof(HttpWorkerRequest));
//bool urlRewriterIsEnabled = (httpWorkerRequest != null && httpWorkerRequest.GetServerVariable(UrlRewriterEnabledServerVar) != null);
bool urlRewriterIsEnabled = (httpWorkerRequest != null && httpWorkerRequest.GetServerVariable("IIS_UrlRewriteModule") != null);
_urlRewriterIsTurnedOnValue = urlRewriterIsEnabled;
_urlRewriterIsTurnedOnCalculated = true;
}
}
}
return _urlRewriterIsTurnedOnValue;
}
In summary, If both requestWasRewritten and IsUrlRewriterTurnedOn
return true, that means one of your web server has IIS Rewrite Module
turned on and running while the other one doesn't have.
For more details on ASP.NET MVC source codes, please refer to this link:
http://aspnetwebstack.codeplex.com/
Hope it helps!

Related

Make ASP.NET Core server (Kestrel) case sensitive on Windows

ASP.NET Core apps running in Linux containers use a case sensitive file system, which means that the CSS and JS file references must be case-correct.
However, Windows file system is not case sensitive. Therefore during development you can have CSS and JS files referenced with incorrect casing, and yet they work fine. So you won't know during development on Windows, that your app is going to break when going live on Linux servers.
Is there anyway to make Kestrel on Windows case sensitive, so that we can have consistent behaviour and find the reference bugs before going live?
I fixed that using a middleware in ASP.NET Core.
Instead of the standard app.UseStaticFiles() I used:
if (env.IsDevelopment()) app.UseStaticFilesCaseSensitive();
else app.UseStaticFiles();
And defined that method as:
/// <summary>
/// Enforces case-correct requests on Windows to make it compatible with Linux.
/// </summary>
public static IApplicationBuilder UseStaticFilesCaseSensitive(this IApplicationBuilder app)
{
var fileOptions = new StaticFileOptions
{
OnPrepareResponse = x =>
{
if (!x.File.PhysicalPath.AsFile().Exists()) return;
var requested = x.Context.Request.Path.Value;
if (requested.IsEmpty()) return;
var onDisk = x.File.PhysicalPath.AsFile().GetExactFullName().Replace("\\", "/");
if (!onDisk.EndsWith(requested))
{
throw new Exception("The requested file has incorrect casing and will fail on Linux servers." +
Environment.NewLine + "Requested:" + requested + Environment.NewLine +
"On disk: " + onDisk.Right(requested.Length));
}
}
};
return app.UseStaticFiles(fileOptions);
}
Which also uses:
public static string GetExactFullName(this FileSystemInfo #this)
{
var path = #this.FullName;
if (!File.Exists(path) && !Directory.Exists(path)) return path;
var asDirectory = new DirectoryInfo(path);
var parent = asDirectory.Parent;
if (parent == null) // Drive:
return asDirectory.Name.ToUpper();
return Path.Combine(parent.GetExactFullName(), parent.GetFileSystemInfos(asDirectory.Name)[0].Name);
}
Based on #Tratcher proposal and this blog post, here is a solution to have case aware physical file provider where you can choose to force case sensitivity or allow any casing regardless of OS.
public class CaseAwarePhysicalFileProvider : IFileProvider
{
private readonly PhysicalFileProvider _provider;
//holds all of the actual paths to the required files
private static Dictionary<string, string> _paths;
public bool CaseSensitive { get; set; } = false;
public CaseAwarePhysicalFileProvider(string root)
{
_provider = new PhysicalFileProvider(root);
_paths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public CaseAwarePhysicalFileProvider(string root, ExclusionFilters filters)
{
_provider = new PhysicalFileProvider(root, filters);
_paths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public IFileInfo GetFileInfo(string subpath)
{
var actualPath = GetActualFilePath(subpath);
if(CaseSensitive && actualPath != subpath) return new NotFoundFileInfo(subpath);
return _provider.GetFileInfo(actualPath);
}
public IDirectoryContents GetDirectoryContents(string subpath)
{
var actualPath = GetActualFilePath(subpath);
if(CaseSensitive && actualPath != subpath) return NotFoundDirectoryContents.Singleton;
return _provider.GetDirectoryContents(actualPath);
}
public IChangeToken Watch(string filter) => _provider.Watch(filter);
// Determines (and caches) the actual path for a file
private string GetActualFilePath(string path)
{
// Check if this has already been matched before
if (_paths.ContainsKey(path)) return _paths[path];
// Break apart the path and get the root folder to work from
var currPath = _provider.Root;
var segments = path.Split(new [] { '/' }, StringSplitOptions.RemoveEmptyEntries);
// Start stepping up the folders to replace with the correct cased folder name
for (var i = 0; i < segments.Length; i++)
{
var part = segments[i];
var last = i == segments.Length - 1;
// Ignore the root
if (part.Equals("~")) continue;
// Process the file name if this is the last segment
part = last ? GetFileName(part, currPath) : GetDirectoryName(part, currPath);
// If no matches were found, just return the original string
if (part == null) return path;
// Update the actualPath with the correct name casing
currPath = Path.Combine(currPath, part);
segments[i] = part;
}
// Save this path for later use
var actualPath = string.Join(Path.DirectorySeparatorChar, segments);
_paths.Add(path, actualPath);
return actualPath;
}
// Searches for a matching file name in the current directory regardless of case
private static string GetFileName(string part, string folder) =>
new DirectoryInfo(folder).GetFiles().FirstOrDefault(file => file.Name.Equals(part, StringComparison.OrdinalIgnoreCase))?.Name;
// Searches for a matching folder in the current directory regardless of case
private static string GetDirectoryName(string part, string folder) =>
new DirectoryInfo(folder).GetDirectories().FirstOrDefault(dir => dir.Name.Equals(part, StringComparison.OrdinalIgnoreCase))?.Name;
}
Then in Startup class, make sure you register a provider for content and web root as follow:
_environment.ContentRootFileProvider = new CaseAwarePhysicalFileProvider(_environment.ContentRootPath);
_environment.WebRootFileProvider = new CaseAwarePhysicalFileProvider(_environment.WebRootPath);
It was possible in Windows 7 but not windows 10 and as far as I can tell, it's also not possible on Windows Server at all.
I can only talk about the OS because the Kestrel documentation says:
The URLs for content exposed with UseDirectoryBrowser and UseStaticFiles are subject to the case sensitivity and character restrictions of the underlying file system. For example, Windows is case insensitive—macOS and Linux aren't.
I'd recommend a convention for all filenames ("all lowercase" usually works best). And to check for inconsistencies, you can run a simple PowerShell script that uses regular expressions to check for wrong casing. And that script can be put on a schedule for convenience.

Can this be done using ASP.NET URL Rewriting

There are about 1700 articles listed on my website created using ASP.NET 4.0 Web Forms. These articles have a url format as:
http://www.mymymyarticles.com/Article.aspx?ID=400
I have explored ASP.NET Friendly URLs as well IIS URL Rewrite. These extensions are great but once a rule is created, they treat all url's generically.
Is it possible that I manually generate my own url string for every url that exists on my website? For eg:
I want to permanent redirect http://www.mymymyarticles.com/Article.aspx?ID=400 to http://www.mymymyarticles.com/this-is-a-very-long-url whereas
http://www.mymymyarticles.com/Article.aspx?ID=500 can be redirected to http://www.mymymyarticles.com/article/short-url and
http://www.mymymyarticles.com/Article.aspx?ID=523 can be redirected to http://www.mymymyarticles.com/very-short-url
So you can see there is no uniformity in the url's that I want to manually generate. Basically I want full control over the url's. How can I go about this. Will it affect performance?
Any examples are appreciated.
Do you have a way of mapping an ID to the url of the new page? If that is the case, you could probably achieve this with ASP.NET Routing. What I would do is start with defining a route:
var route = routes.MapRoute(
"LegacyDocument",
"Articles.aspx{*pathInfo}",
null,
constraints: new { pathInfo = new LegacyDocumentRouteConstraint() }
);
route.RouteHandler = new RedirectRouteHandler();
This route merely captures any requests for /articles.aspx, but it has a constraint and a custom route handler.
The purpose of the constraint is to ensure we at least have the ID query string property and it is a number:
public class LegacyDocumentRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
// Don't both doing anything while we generate links.
return false;
}
string id = httpContext.Request.QueryString["id"];
if (string.IsNullOrWhiteSpace(id))
{
// No query string argument was provided.
return false;
}
int documentId;
if (!int.TryParse(id, out documentId))
{
// The Id is not a number.
return false;
}
// Set the output document Id in the route values.
values["documentId"] = documentId;
return true;
}
}
If the Id was not provided, or was not a number, we can't match to an existing document, so the route will be skipped over. But when the constraint is satisfied, we store a variable in the route values values["documentId"] = documentId so we can then use it again (without having to parse it from the query string again) later in the route handler:
public class RedirectRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext context)
{
int documentId = (int)context.RouteData.Values["documentId"];
return new RedirectLegacyDocumentHttpHandler(documentId);
}
private class RedirectLegacyDocumentHttpHandler : IHttpHandler
{
private int _documentId;
public RedirectHttpHandler(int documentId)
{
_documentId = documentId;
}
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context)
{
var response = context.Response;
string url = ResolveNewDocumentUrl();
if (url == null)
{
// Issue a 410 to say the document is no longer available?
response.StatusCode = 410;
}
else
{
// Issue a 301 to the new location.
response.RedirectPermanent(url);
}
}
public string ResolveNewDocumentUrl()
{
// Resolve to a new url using _documentId
}
}
}
Route handlers perform the logic of mapping from ASP.NET routing back into the IHttpHandler logic of the ASP.NET runtime. In normal MVC, this would map to the standard MvcHandler which invokes controllers, but in our case, we need only to issue a redirect.
In the route handler, we grab our document Id from the route values, and create a new HTTP handler which performs the actual redirect. You'll need to plumb in the bit which you resolve what the actual new url would be (ResolveNewDocumentUrl), but generally it will resolve the url, if the url comes back as null, we'll issue a HTTP 410 Gone response to say to clients (and more importantly crawlers) that the item is no longer there, or it will issue an HTTP 301 Permanent Redirect with the appropriate location header to the new url.
I had overcome this by creating an xml file on the server with the below schema
<URLMapper>
<Code>1</Code>
<OLDURL>%Oldurl.aspx%</OLDURL>
<NEWURL>default.aspx</NEWURL>
<PermanentRedirect>true</PermanentRedirect>
<Order>1</Order>
<Status>true</Status>
</URLMapper>
Loaded this in Application_Start Event to an application variable (in the form of a datatable).
And in the Begin Request --
void Application_BeginRequest(object sender, EventArgs e)
{
if (Application["URLMapper"] == null) return;
DataTable dtURLs = Application["URLMapper"] as DataTable;
if (dtURLs.Rows.Count == 0) return;
string OrigUrl = HttpContext.Current.Request.Url.ToString().ToLower().Replace("'", "`");
DataRow[] drFound = dtURLs.Select("Status = true and '" + OrigUrl.Trim() + "' like oldurl", "Order",DataViewRowState.CurrentRows);
if (drFound.Length == 0) return;
string OldURL = drFound[0]["OldURL"].ToString().Replace("%","");
Response.RedirectPermanent(OrigUrl.Replace(OldURL, drFound[0]["NewURL"].ToString().Trim()), true);
return;
}

nopCommerce on AppHarbor. Redirect loop

I am trying to deploy a nopCommerce application to AppHarbor.
When I start the page I run into a runtime redirect loop however. I added a bit of debug logging and the problem seems to be this part in Global.asax.cs -> EnsureDatabaseIsInstalled():
if (!webHelper.GetThisPageUrl(false).StartsWith(installUrl, StringComparison.InvariantCultureIgnoreCase))
{
this.Response.Redirect(installUrl);
}
StartsWith comparison is always false because
GetThisPageUrl returns
http://[name].apphb.com:14275/install
and installUrl (via GetStoreLocation) returns
http://[name].apphb.com/install
Has anyone been able to make nopCommerce work with AppHarbor at all?
It looks like you will need to modify nopCommerce to omit the port number. I took a quick look at the source and there seems to be two possible solutions:
1) Changing the boolean argument from false to true in the EnsureDatabaseIsInstalled method should cause the GetThisPageUrl method to pick a different branch that generates the URL without the port number.
2) Updating the else branch in the GetThisPageUrl method (of "WebHelper.cs") to ignore the port number.
It's easier to pick the first solution, but patching the issue at its core will be better so you don't run into similar issues.
In addition to #TroelsThomsen fix, we use a wrapper in our base controller to ensure that all of our code is oblivious to appharbor port changing.
First, #TroelsThomsen fix in Webhelper.cs:75
public virtual string GetThisPageUrl(bool includeQueryString, bool useSsl)
{
string url = string.Empty;
if (_httpContext == null)
return url;
if (includeQueryString)
{
string storeHost = GetStoreHost(useSsl);
if (storeHost.EndsWith("/"))
storeHost = storeHost.Substring(0, storeHost.Length - 1);
url = storeHost + _httpContext.Request.RawUrl;
}
else
{
#if DEBUG
var uri = _httpContext.Request.Url;
#else
//Since appharbor changes port number due to multiple servers, we need to ensure port = 80 as in AppHarborRequesWrapper.cs
var uri = new UriBuilder
{
Scheme = _httpContext.Request.Url.Scheme,
Host = _httpContext.Request.Url.Host,
Port = 80,
Path = _httpContext.Request.Url.AbsolutePath,
Fragment = _httpContext.Request.Url.Fragment,
Query = _httpContext.Request.Url.Query.Replace("?", "")
}.Uri;
#endif
url = uri.GetLeftPart(UriPartial.Path);
}
url = url.ToLowerInvariant();
return url;
}
So what we did is simply add files from https://gist.github.com/1158264 into Nop.Core\AppHarbor
and modified base controllers:
nopcommerce\Presentation\Nop.Web\Controllers\BaseNopController.cs
public class BaseNopController : Controller
{
protected override void Initialize(RequestContext requestContext)
{
//Source: https://gist.github.com/1158264
base.Initialize(new RequestContext(new AppHarborHttpContextWrapper(System.Web.HttpContext.Current),
requestContext.RouteData));
}
//Same file from here downwards...
}
nopcommerce\Presentation\Nop.Web.Admin\Controllers\BaseNopController.cs
public class BaseNopController : Controller
{
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
//set work context to admin mode
EngineContext.Current.Resolve<IWorkContext>().IsAdmin = true;
//Source: https://gist.github.com/1158264
base.Initialize(new RequestContext(new AppHarborHttpContextWrapper(System.Web.HttpContext.Current), requestContext.RouteData));
//base.Initialize(requestContext);
}
//Same file from here downwards...
}

How do I get ASP.NET WebForms Routing to route .asmx JSON calls properly?

I am attempting to implement multi-tenancy in a legacy ASP.NET WebForms app. I want the URL to indicate the proper client, like so:
http://example.com/client_name/Default.aspx
http://example.com/client_name/MyWebService.asmx
However, I cannot get it to route the .asmx's properly. This routing rule picks up all incoming urls just fine:
routes.Add("ClientSelector", new System.Web.Routing.Route
(
"{client}/{*path}",
routeHandler: new ClientRoute()
));
But I am having issues with handling .asmx calls. Here's my IRouteHandler, below. The error I get is:
A first chance exception of type 'System.Web.Services.Protocols.SoapException' occurred in System.Web.Services.dll
Additional information: Unable to handle request without a valid action parameter. Please supply a valid soap action.
It's supposed to be JSON, but for some reason it's not working. I am setting the content-type - if I send this same exact request without routing, it works fine.
public class ClientRoute : System.Web.Routing.IRouteHandler
{
private string m_Path;
private string m_Client;
public ClientRoute() { }
public bool IsReusable { get { return true; } }
public IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
this.m_Path = (string)requestContext.RouteData.Values["path"];
this.m_Client = (string)requestContext.RouteData.Values["client"];
string virtualPath = "~/" + this.m_Path;
bool shouldValidate = false;
if (shouldValidate && !UrlAuthorizationModule.CheckUrlAccessForPrincipal(
virtualPath, requestContext.HttpContext.User,
requestContext.HttpContext.Request.HttpMethod))
{
requestContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
requestContext.HttpContext.Response.End();
return null;
}
else
{
HttpContext.Current.RewritePath(virtualPath);
HttpContext.Current.Items.Add("Client", this.m_Client);
if (virtualPath.EndsWith(".aspx"))
return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page));
else
{
var asmxPos = virtualPath.IndexOf(".asmx", StringComparison.OrdinalIgnoreCase);
if (asmxPos >= 0)
{
// What goes here? This isn't working...
var asmxOnlyVirtualPath = virtualPath.Substring(0, asmxPos + 5);
return new System.Web.Services.Protocols.WebServiceHandlerFactory().GetHandler(
HttpContext.Current, HttpContext.Current.Request.HttpMethod, asmxOnlyVirtualPath, HttpContext.Current.Server.MapPath(asmxOnlyVirtualPath));
}
else
return new StaticRoute();
}
}
}
}
Relevant links:
Getting ScriptHandlerFactory handler
The open source http://www.teamlab.com project is built with ASP.NET Webforms, and uses a multitenant/saas model. I noticed you posted another question inquiring about multitenancy.
Perhaps you can look into their code for reference ideas.
I tried my best, ended up failing, and converted all my web services to WCF .svc services instead.

Output Caching using BOTH varybyparam and varybycustom

I'm trying to do something which should be very simple...I have a site with a dropdown from which the user selects a group. Thereafter, the user navigates through the site using querystring arguments from menus. So I want the caching to be dependent on the querystring - this seems to work. I also want the cache to be dependent on the group that they selected.
But when the querystring is empty, neither cache element seems to work - the page is just whatever the version was for the last selected group. My cache directive looks like this:
<%# OutputCache Duration="300" VaryByCustom="currentAtomId" VaryByParam="documentId;folderId;sectionId;renderMode;typeId" %>
My varyByCustom code looks like this:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
switch (custom)
{
case "currentAtomId":
var currentAtomId = SecurityManifold.Create().CurrentAtomId;
var returnString = currentAtomId == null ? Guid.NewGuid().ToString() : currentAtomId.ToString();
return returnString;
default:
throw new ArgumentException(string.Format("Argument '{0}' is not a valid cache argument.", custom));
}
}
The call to CurrentAtomId boils down to this:
public static int? GetCurrentAtomIdFromContext(HttpContext context)
{
int entityId;
if (context.Session == null)
{
throw new InvalidOperationException("Session is null");
}
var sessionEntityId = context.Session["CurrentEntityId"];
if (sessionEntityId == null || string.IsNullOrEmpty(sessionEntityId.ToString()))
{
return null;
}
if (!int.TryParse(sessionEntityId.ToString(), out entityId))
{
return null;
}
return entityId;
}
Finally, the code which specifies the CurrentEntityId is this:
var selectedEntityId = this.lstSecurityEntities.SelectedValue;
if (string.IsNullOrEmpty(selectedEntityId))
{
return;
}
Session["CurrentEntityId"] = selectedEntityId;
var possibleQueryString = Request.QueryString.ToString();
if (!string.IsNullOrEmpty(possibleQueryString))
{
possibleQueryString = "?" + possibleQueryString;
}
Response.Redirect("default.aspx" + possibleQueryString);
I'm baffled. Any thoughts would be appreciated.
I eventually determined the problem - when output caching is placed at a PAGE level (as opposed to a control level), the session is not available, and throws an exception. Because this exception is occurring in Global ABOVE the global error handler, it fails silently. I eventually figured this out by wrapping a try-catch block around the cache key generation code in VaryByCustomString and Response.Write-ing it out.
What a beatdown...at any rate, the solution is to implement caching at the control level, which unfortunately is a lot more work because the pieces of the page work together...but it's better than no caching. I hope this helps save somebody else some time.
Bottom Line: for varyByCustomString in global.asax - SESSION IS NOT AVAILABLE WHEN CACHING AT THE PAGE LEVEL.

Resources