I have an area in my ASP.NET MVC 5 application that's defined as follows. Notice the second "alias" route I'm trying to setup.
public class MyAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "My";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"My_default",
"My/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
context.MapRoute(
"My_Legal",
"Legal_Stuff",
new { controller = "Home", action = "Index" }
);
}
}
}
My client requires a specific URL for accessing the page, /Legal_Stuff
The following URL works fine on my workstation with IIS Express launched by Visual Studio 2013:
localhost:XXXX/Legal_Stuff
However, once I place the app to a Windows 2008 R2 server (IIS), the URL gives "Page Not Found"
Once I add a trailing slash, it works.
www.mysite.com/Legal_Stuff -- Page Not Found
www.mysite.com/Legal_Stuff/ -- works
However my client doesn't want the trailing slash.
Is there a way to overcome this problem?
I searched stackoverflow for solution and found some people recommending the IIS Rewrite module. However, none of the solutions worked for me.
Any help is greatly appreciated.
URL Rewrite IS the more efficient way to do this, but tends to be more technical demanding.
Here is a friendly article that can help you with that:
http://www.iis.net/learn/extensions/url-rewrite-module/creating-rewrite-rules-for-the-url-rewrite-module
Another easier way it so to redirect in your IIS Manager -> Click on your website -> Use the HTTP Redirect feature to redirect to your website to the same website containing a containing slash.
URL Example : www.mysite.com/Legal_Stuff/
Keep in mind that the server will redirect to your address and will result in response 302 in case someone would try to access your website.
Thanks for the link.
Here's the solution:
<rewrite>
<rules>
<rule name="Legal_Stuff" patternSyntax="ExactMatch">
<match url="/Legal_Stuff" />
<action type="Rewrite" url="/LegalStuff/" />
</rule>
</rules>
</rewrite>
Related
I have developed an ASP MVC 5 website with the following route:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{language}",
defaults: new
{
controller = "Member",
action = "Index",
language = UrlParameter.Optional
});
}
I only have that one controller, Member, and everything works running on IISExpress on my local dev machine. But when I try to deploy and access the site on my development server, I get 404 error. The URL I'm passing to it is identical to the one I'm using on the development machine, yet it seems like the routing is not working as expected. Here's a sample URL:
http://myserver:8080/Member/GetCertificate/en-US?mn=MjMzOTA3MDc4MDA=&gn=NjcwNzkz
This is the only route registered on my application, and I've tried to register the wildcard script on IIS, as well as editing my web config with the following entries:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<remove name="UrlRoutingModule-4.0"/>
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/>
</modules>
</system.webServer>
The development server is running IIS 7.5. I have tried to access the page remotely from my development machine as well as locally from the web server, with no luck. Here's what the action method looks like for reference:
[HttpGet]
public FileResult GetCertificate(string language, string mn, string gn)
{
var member = new Member()
{
MemberNumber = Encoding.Default.GetString(
Convert.FromBase64String(mn)),
GroupNumber = Encoding.Default.GetString(
Convert.FromBase64String(gn)),
Language = language
};
var certificate = this.certificateRepository
.GetCertificateDocument(member);
return this.File(certificate, "application/pdf");
}
I have continued debugging it, and I found that the problem seems to be that it's recognizing the action as the controller, so if I use the following URL, it works:
http://myserver:8080/Member/Member/GetCertificate/en-US?mn=MjMzODU1NjE5MDE=&gn=NzkxMjgz
But I end up repeating the controller name, which makes for a not so readable URL. Any way around this, though? Maybe I missed something?
The problem was that my application within IIS had a name conflict with my controller. The structure in my IIS was as follows:
MemberSite
Member
MemberService
And the controller in my MVC application was also called MemberController. This caused confusion, because I thought I was referencing the controller, when I was actually referencing the application itself. So for readability, I renamed the application in IIS to MemberApp, and the final URL looks like this:
http://myserver:8080/MemberApp/Member/GetCertificate/en-US?mn=MjMzODU1NjE5MDE=&gn=NzkxMjgz
I am just learning ASP.NET MVC, and I have recently found the [RequireHttps] attribute to automatically redirect a GET request to use SSL, like so...
[RequireHttps] //apply to all actions in controller
public class SomeController
{
[RequireHttps] //apply to this action only
public ActionResult SomeAction()
{
...
}
}
When using IIS Express as the development server, this successfully redirects the request from http://localhost:55945/... to https://localhost/....
However on my development system, my project is using HTTPS on port 44300 (this was configured automatically by Visual Studio 2010), and I have not yet found any way to tell MVC to use that port number in the redirection, so that it goes to the required https://localhost:43300/... instead.
I really expected this to be automatic, given the SSL port number was set automatically by Visual Studio, and as far as I can tell, this must be effecting all developers using the [RequireHttps] attribute in MVC3. In my searching for a solution, I have seen a few patchy "work around" solutions, but nothing that appears to be definitively "the right way" to fix it.
So doing things "the right way", what do I change (either in the source code, or in my project configuration) to tell the [RequireHttps] attribute in MVC3 to use the HTTPS port that my project is configured to be using?
Or alternatively, is there some other completely different and better "right way" to set up SSL support in an MVC3 project, that does not have this problem?
The RequireHttpsAttribute is quite simple and it cannot be parametrized to redirect to a specific port. If you really need this you could create a subclass and override the HandleNonHttpsRequest method to compose the redirect url differently.
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext) {
base.HandleNonHttpsRequest(filterContext);
// redirect to HTTPS version of page
string url = "https://" + filterContext.HttpContext.Request.Url.Host + ":" + MyConfig.SslPort + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
However, if your entire site runs with HTTPS you could just configure in VS in the web project properties (Web -> Start Action -> Start URL) to open the correct HTTPS url with your port and not use the redirect feature for local testing.
I have this same issue, and I'm solving it by using both the RequireHttpsAttribute and a URL rewrite rule in Web.config. The rewrite rule matches the nonstandard port numbers, and executes ahead of the attribute. You can use a Web.config transform to remove the rewrite rule on deployment, but if you leave it in it shouldn't have any effect. In production you'll use standard port number that the rule won't match. Then the attribute will catch it.
Here's the rule:
<system.webServer>
<rewrite>
<rules>
<!-- Redirect HTTP requests to HTTPS, using the non-standard development ports.
On deployment, this rule is removed, and the RequireHttpAttribute filter globally applied
in SlicerWeb.FilterConfig takes over. This rewrite rule executes before the attribute
would be applied, and so can apply the port numbers -->
<rule name="HTTPS redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
<add input="{SERVER_PORT}" pattern="60470" />
</conditions>
<action type="Redirect" url="https://{SERVER_NAME}:44300/{R:1}" redirectType="Found" />
</rule>
</rules>
</rewrite>
</system.webServer>
The downside to this approach is that you're not relying on the attribute when running locally. But if you've applied it once globally, instead of adding it to every controller, that's OK in my opinion.
A few things that might be helpful.
There is a version of the source code of RequireHttpsAttribute in this thread: Where is the source for RequireHttpsAttribute?
There is also a nearly identical class called RequireSslAttribute on codeplex mentioned on the same thread. http://aspnet.codeplex.com/SourceControl/changeset/view/63930#391756
Here is a sample of an attribute that can be used to toggle either from http to https or the reverse based on the TargetUriScheme property. It also includes properties for specifying port numbers.
I have chosen to use an #if DEBUG block in my constructor to set my local development ports when I am building under the Debug configuration. This works for me since I always build under release when deploying, in which case the port numbers will default to null and will be left out of the url's.
The port numbers can also be set when applying the attribute to an action method. I could also see hooking these to a config file or some other configuration source to determine the port numbers at runtime.
public class ToggleHttpHttpsAttribute : FilterAttribute, IAuthorizationFilter
{
//supported uri scheme values
public enum UriScheme
{
Http,
Https
}
public ToggleHttpHttpsAttribute(
UriScheme uriScheme = UriScheme.Http)
{
TargetUriScheme = uriScheme;
#if DEBUG
//set DEBUG ports
HttpPort = 55892;
HttpsPort = 44301;
#endif
}
private UriScheme TargetUriScheme { get; set; }
public int? HttpPort { get; set; }
public int? HttpsPort { get; set; }
public void OnAuthorization(AuthorizationContext filterContext)
{
if(filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
bool isHttps = filterContext.HttpContext.Request.IsSecureConnection;
if ((isHttps && TargetUriScheme == UriScheme.Http) || (!isHttps && TargetUriScheme == UriScheme.Https))
{
ToggleUriScheme(filterContext);
}
}
private void ToggleUriScheme(AuthorizationContext filterContext)
{
//only allow toggle if GET request
if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("ToggleHttpHttpsAttribute can only be used on GET requests.");
}
filterContext.Result = GetRedirectResult(filterContext);
}
private RedirectResult GetRedirectResult(AuthorizationContext filterContext)
{
string prefix = TargetUriScheme == UriScheme.Http
? "http://"
: "https://";
int? port = TargetUriScheme == UriScheme.Http
? HttpPort
: HttpsPort;
string url = string.Format(
"{0}{1}{2}{3}",
prefix,
filterContext.HttpContext.Request.Url.Host,
port == null ? "" : string.Format(":{0}", port),
filterContext.HttpContext.Request.RawUrl);
return new RedirectResult(url);
}
}
I'm quite puzzled as to why I have had to spend hours trying to get this right. I did a URL rewrite similar to the way Scott Hanselman explains it here:
http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx
This works perfectly and I just take out the rewrite rule on my production web server.
I have an ASP.NET / C# website that's hosted on Windows Azure. The site is a predictions-based social site with a feed of prediction summaries on the main page. If you click on a summary, you're redirected to the details page for that prediction using a simple QueryString.
For example:
http://www.ipredikt.com/details.aspx?id=14
This particular prediction is entitled "Paris Hilton will win the Nobel Peace Prize" so what I'd like to do is implement URL rewriting for my site on Azure as follows:
http://www.ipredikt.com/predictions/14/paris-hilton-will-win-the-nobel-peace-prize
What are some strategies and best practices for doing this? And can someone point me to a good Azure-specific article or two.
The hyphenated title ("paris-hilton-bla-bla") is really just to make the URL more human readable; I don't envision relying on it at all in terms of loading pages. In fact, I'd probably allow duplicate titles since I'll be relying on the prediction ID in the URL.
EDIT:
Forgot to mention that we are NOT based on MVC. We came up w/ our own architecture that uses PageMethods and WebMethods to return JSON to the client. We rely on ASP.NET AJAX to do all of the JSON serialization, and almost all of our UI is built dynamically on the client using jQuery.
EDIT: SOLUTION
Thought I'd share my solution now that I have things up and running.
I made a new class as follows (copied verbatim from somewhere):
public class WebFormRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
public string VirtualPath { get; set; }
public WebFormRouteHandler(string virtualPath)
{
this.VirtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return (VirtualPath != null)
? (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(VirtualPath, typeof(T))
: new T();
}
}
I added the following method to Global.asax. The actual method is MUCH, much longer (it covers every page in the site). You'll see that I support calling the predictions page in lots of different ways: with an id, with an id + title, etc. (The "...fb" versions of pages are for the Facebook app version of my site which use a different MasterPage.)
public static void RegisterRoutes(RouteCollection routes)
{
// Details : 'predictions' Page
var routeHandlerDetails = new WebFormRouteHandler<Page>("~/details.aspx");
var routeHandlerDetailsFb = new WebFormRouteHandler<Page>("~/detailsfb.aspx");
routes.Add(new Route("predictions/{id}", routeHandlerDetails));
routes.Add(new Route("predictions/{id}/{title}", routeHandlerDetails));
routes.Add(new Route("fb/predictions/{id}", routeHandlerDetailsFb));
routes.Add(new Route("fb/predictions/{id}/{title}", routeHandlerDetailsFb));
}
...and this method is called from Application_Start()
void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
Then I added the following to web.config in the system.webServer block:
<!-- Added for URL Routing -->
<modules runAllManagedModulesForAllRequests="true">
<add name="UrlRoutingModule"
type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</modules>
<!-- Added for URL Routing -->
<handlers>
<add name="UrlRoutingHandler"
preCondition="integratedMode"
verb="*"
path="UrlRouting.axd"
type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</handlers>
I also had to exclude the virtual "predictions" directory from authentication (because almost all parts of our site are accessible my non-auth users):
<!-- Url routing -->
<location path="predictions">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
Finally, I no longer rely on QueryString string parameters when loading pages, so I had to write some new helper methods. Here's one that extracts a numerical value from the new routing URL (I'll be cleaning this up to only have a single 'return'.):
public static int GetRouteDataValueAsNumber(HttpRequest request, string propertyName)
{
if ((request == null) ||
(request.RequestContext == null) ||
(request.RequestContext.RouteData == null) ||
(request.RequestContext.RouteData.Values[propertyName] == null))
{
return -1;
}
try
{
return System.Convert.ToInt32(request.RequestContext.RouteData.Values[propertyName]);
}
catch
{
}
return -1;
}
Now when I need to read a routing value (like a prediction ID), I do the following:
long _predictionId = System.Convert.ToInt64(WebAppUtils.GetRouteDataValueAsNumber(Request, "id"));
Works great! Now my site feels like an MVC app with friendly and self-documenting URLs.
Oh, last thing, you also need to enable HTTP Redirection as follows:
Start => Control Panel => Program => Turns Windows Features On => Internet Information Services => World Wide Web Services => Common HTTP Features => (select checkbox for) HTTP Redirection.
The easiest way to implement this would be a programmatic approach using the System.Web.Routing assembly.
This basically works by including the UrlRoutingModule in your web.config, and defining patterns that resolve the target page based on matching routes. If you are familiar with ASP.NET MVC, then you have used this routing strategy before, but MVC is not necessary to use Routing.
Here are some resources to help you get started:
MSDN Documentation for the System.Web.Routing namespace - official documentation
Scott Gu on URL Routing for MVC - * Note that this article explains routing in the context of an ASP.NET MVC application, however, the same methodology will work regardless of whether or not you are using MVC
ASP.NET Routing... Goodbye URL rewriting, by Chris Cavanagh - An explanatory article
Exploring System.Web.Routing, by Justin Etheredge - A case study explaining how to use routing independently of the MVC architecture
About Windows Azure ...
If you take this approach, it doesn't really matter that you are using Windows Azure. However, I found an article by Michael Kennedy called ASP.NET Routing in Windows Azure Using WebForms, explaining how to easily deploy such a solution on Windows Azure. The article even has a sample project for download.
Azure web roles have the IIS7 Url Rewriting module installed - http://msdn.microsoft.com/en-us/library/dd573358.aspx
The "how to" for this module is at http://learn.iis.net/page.aspx/460/using-the-url-rewrite-module/
For your Paris example, you basically need to setup a rule that maps the url
http://www.ipredikt.com/predictions/14/paris-hilton-will-win-the-nobel-peace-prize
to
http://www.ipredikt.com/details.aspx?id=14
This is something like:
Pattern -
^predictions/([0-9]+)/([_0-9a-z-]+)
Action -
details.aspx?id={R:1}
For more on defining these rules see http://learn.iis.net/page.aspx/461/creating-rewrite-rules-for-the-url-rewrite-module/
So I have a route like this in my MVC 3 application running under IIS 7:
routes.MapRoute(
"VirtualTourConfig",
"virtualtour/config.xml",
new { controller = "VirtualTour", action = "Config" }
);
The trick is that a file actually exists at /virtualtour/config.xml. It seems like the request is just returning the xml file at that location instead of hitting the route, which processes the XML, makes some changes and returns a custom XmlResult.
Any suggestions on how I can tell my application to hit the route and not the actual file in the event that the file exists on disk?
EDIT: It appears that I can use routes.RouteExistingFiles = true; in the RegisterRoutes method of Global.asax to tell the application to ignore files on disk. This, however, sets the flag globally and breaks a lot of other requests within the application. For example, I still want calls to /assets/css/site.css to return the CSS file without having to specifically set routes up for each static asset. So now the question becomes, is there a way to do this on a per-route basis?
So far the best answer to this that I have found is to globally apply routes.RouteExistingFiles=true and then selectively ignore the routes I want to pass through to existing files like .js, .css, etc. So I ended up with something like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("*.js|css|swf");
routes.RouteExistingFiles = true;
routes.MapRoute(
"VirtualTourConfig",
"virtualtour/config.xml",
new { controller = "VirtualTour", action = "Config" }
);
}
If anyone has a better solution, I'd like to see it. I'd much prefer to selectively apply an "RouteExistingFIles" flag to individual routes but I don't know if there's a way to do that.
No solution here, just an idea.
You can try to implement solution based on your own VirtualPathProvider that will provide your own mapping of web paths to file system paths and use default provider for all paths you don't want to take care of.
A very simple solution, certainly if you consider Scott's answer, would be eg: routes.IgnoreRoute("templates/*.html");,
routes.IgnoreRoute("scripts/*.js"); and routes.IgnoreRoute("styles/*.css");.
This gives you both the simplicity of just supplying paths to the RoutesCollection and avoids the work of having to initiate eg. a VirtualPathProvider.
You could try UrlRewiting e.g.:
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="VirtualTourConfig">
<match url="^virtualtour/config.xml" />
<action type="Rewrite" url="virtualtour/config" />
</rule>
</rules>
</rewrite>
NB I'm using this for a different use-case (i.e. serving an angular app from asp.net MVC) - I haven't tested whether MVC routing occurs AFTER url rewriting.
In my case I have a normal MVC route (i.e. /Dashboard/ => DashboardController.Index()) but need all relative paths in the Views/Dashboard/Index.cshtml to serve static files without getting confused by MVC routing i.e. /Dashboard/app/app.module.js must serve a static file. I use UrlRewriting to map /Dashboard/(.+) to /ng/Dashboard/{R:1} as follows:
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Dashboard">
<match url="^dashboard/(.+)" />
<action type="Rewrite" url="ng/dashboard/{R:1}" />
</rule>
</rules>
</rewrite>
I'm trying to setup Forms Authentication in an asp.net mvc 2 application that will be hosted on IIS 6. There's an issue somewhere in my routing, but I can't pinpoint exactly where it is.
Here is the route entries I'm using to route the mvc requests through the aspx processing on IIS 6. These may or may not be the "right" way, but they do work on the server at current.
routes.MapRoute(
"Default",
"{controller}.aspx/{action}/{id}",
new { action = "LogOn", id = "" }
);
routes.MapRoute(
"Root",
"",
new { controller = "Main", action = "LogOn", id = "" }
);
I've put the [Authorize] attribute on my Main controller.
In my web.config I have:
<authentication mode="Forms">
<forms loginUrl="~/Main.aspx/LogOn" timeout="2880"/>
</authentication>
When the application starts, a blank page loads. The page is quite literally blank. I haven't found a way to amend the loginUrl to actually execute the LogOn action & View for my Main controller.
Edited
Just as an fyi, I've setup my routing based on this article so that the mvc routing can work on IIS 6.
http://www.asp.net/mvc/tutorials/using-asp-net-mvc-with-different-versions-of-iis-cs
I'm guessing the problem here is that the windows form authentication settings aren't syncing with the routes setup so the app can run on IIS 6 via the aspx extension.
Anyone have thoughts on how I could fix this?
Edit 2
Tried adding the following route:
routes.MapRoute(
"Login",
"Login",
new { controller = "Main", action = "LogOn" }
);
Amended the web.config to:
<authentication mode="Forms">
<forms loginUrl="~/Login" timeout="2880"/>
</authentication>
The result is the same white screen as I originally got. It seems like the page doesn't get processed at all. Viewing the source from page generated shows absolutely nothing....no markup...no html declaration....just nothing.
EDIT 3
It seems that I can't seem to get the correct routing configured with the default forms authentication via the web.config. To circumvent this, I've created my own Authorize attribute class. At current, I only care that the user has logged into the system. To accomodate this, I moved the LogOn & LogOff actions to an Account controller. I've remapped the root path to point to this controller. In my custom Authorize attribute, I check to see if the user is logged in and redirect them back to the LogOn page if they aren't. Here is the code:
routes.MapRoute(
"Root",
"",
new { controller = "Account", action = "LogOn", id = "" }
);
And here's the code for the RequireLoginAttribute class I derrived.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireLoginAttribute : AuthorizeAttribute, IAuthorizationFilter
{
#region IAuthorizationFilter Members
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.Request.IsAuthenticated)
{
//This didn't work...it would try routing to urls like
//http://localhost:1524/Main.aspx/Account.aspx/Logon
//new RedirectResult("Account.aspx/Logon");
//This seems sloppy to me somehow, but it works.
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Account", action = "LogOn" }));
}
}
#endregion
}
Now, I can just apply the [RequireLogin] attribute to the Main controller and it ensures the user must be authenticated.
For the curious, and completeness of this scenerio, I am using the following code in the LogOn action (repository isn't ready yet so things are hard coded):
public ActionResult LogOn(LogOnModel login, String returnUrl)
{
if (ModelState.IsValid)
{
FormsAuthentication.SetAuthCookie(login.UserName, false);
return Redirect(returnUrl ?? Url.Action("NextPage", "Main"));
}
else
{
return View(login);
}
}
The returnUrl is a throwback to the Windows Forms authentication. Since I can't seem to get that working here, the parameter will always be null.
Please, critique this if you see specific areas that need improvement. I'm reading what I can and trying to do things right, so all input is greatly appreciated. Thanks!
If you need the .aspx for the Default Root then why don't you need it for the login route ?
You could do a couple of things then
Actually create a ASP.NET page called Login.aspx and put it in the root of the folder (Authentication will work for your mvc pages as well)
Change your login route to say
routes.MapRoute("Login","Login.aspx",new { controller = "Main", action = "LogOn" });
You should also take a look at to see what route your actually hitting at any time.
http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx
Remember that the order you write your routes in Global matters. It stops checking when it finds one that works so your catch all should be last.
The details I posted in EDIT 3 summarize the solution to this issue. I appreciate all of your input into this question, but I've resolved it. I would have liked to have gotten the "out of the box" forms authentication working, but this solution serves well enough. If we move to IIS 7, I think all of this will become moot anyhow.
Thanks again for your help guys.