Sitecore custom 404 handler in production - c#

I picked up the following code from Stackoverflow->a blog re handling custom 404 in Sitecore (which acutally does a 302 redirect to 404 page with status 200 which gets picked up by google as soft 404).
While this works totally fine in our local test servers, the moment we drop it in production the site goes haywire and takes AGES e.g. 8-9 minutes to load and stuff.
public class ExecuteRequest : Sitecore.Pipelines.HttpRequest.ExecuteRequest
{
protected override void RedirectOnItemNotFound(string url)
{
var context = System.Web.HttpContext.Current;
try
{
// Request the NotFound page
var domain = context.Request.Url.GetComponents(
UriComponents.Scheme | UriComponents.Host,
UriFormat.Unescaped);
var content = WebUtil.ExecuteWebPage(
string.Concat(domain, url));
// The line below is required for IIS 7.5 hosted
// sites or else IIS is gonna display default 404 page
context.Response.TrySkipIisCustomErrors = true;
context.Response.StatusCode = 404;
context.Response.Write(content);
}
catch (Exception ex)
{
Log.Error(string.Format("Falling back to default redirection behavior. Reason for error {0}", ex), ex);
// Fall back to default behavior on exceptions
base.RedirectOnItemNotFound(url);
}
context.Response.End();
}
}
P.S: I then replaced ExecuteRequest with my custom one in web.config.
If you have experienced similar thing or know of any issue re this please do shed some light.
Thanks in advance

There is a setting in Sitecore, with which you can get rid of the 302 redirect:
<setting name="RequestErrors.UseServerSideRedirect" value="true" />
With this settings, the url stays the same and the status code is 404. If you want to have some additional logic (like showing a Sitecore item as error page), there is a Shared Source module called Error Manager on the Sitecore Market Place.
Hope that helps.

Check if the server is able to access the hostname of your website.
Servers often do not have access to a DNS and therefore are unable to resolve hostnames. In order for your 404 handler to work, the application needs to be able to access its own hostname to request the 404 page.
To be sure this works, edit the hosts file of the server and add an entry for your hostname there, pointing it to 127.0.0.1

You can resolve it with creating new resolver. It is good solution when you want to give to an user error page in right language. But there some differences in IIS 7.0 and 7.5.
Add processor to your sitecore configuration:
<processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel"/>
<processor type="Project.Error404Resolver, Project" />
Processor resolving it:
For IIS 7.0:
public class Error404Resolver : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
{
public override void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
if(Sitecore.Context.Item == null && !args.Context.Request.Url.AbsolutePath.StartsWith("/sitecore")
{
args.Context.Response.Clear();
SiteContext site = Sitecore.Context.Site;
if(site != null)
{
Item item404Page = Sitecore.Context.Database.GetItem(site.RootPath + "website/error/404");
if(item404Page != null)
{
Sitecore.Context.Item = item404Page;
args.Context.Response.StatusCode = (int) System.Net.HttpStatusCode.NotFound;
}
}
}
}
}
For IIS 7.5:
public class Error404Resolver : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
{
public override void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
if(Sitecore.Context.Item == null && !args.Context.Request.Url.AbsolutePath.StartsWith("/sitecore")
{
args.Context.Response.Clear();
SiteContext site = Sitecore.Context.Site;
if(site != null)
{
Item item404Page = Sitecore.Context.Database.GetItem(site.RootPath + "website/error/404");
if(item404Page != null)
{
WebClient webClient = new WebClient();
webClient.Encoding = args.Context.Request.ContentEncoding;
webClient.Headers.Add("User-Agent", args.Context.Request.UserAgent);
string page = webClient.DownloadString(LinkManager.GetItemUrl(item404Page));
args.Context.Response.StatusCode = (int) System.Net.HttpStatusCode.NotFound;
args.Context.Response.Write(page);
args.Context.Response.TrySkipIisCustomErrors = true;
args.Context.Response.End();
}
}
}
}
}
Whit this you will render error page in current page without redirect and returns to a browser code 404.

I have the same issue at a customer I currently work at (looks like the code was pasted) and actually the reason is pretty obvious: If you execute this call with a url that is not registered in the Sitecore sites config (but accessible via IIS), you will also run through this code. Unfortunately, the WebUtil.ExecuteWebPage call is executed with the wrong url as well, hence you end up stuck in a loop.
Actually you should see a lot of these messages in your log: Falling back to default redirection behavior. Reason for error {0}, probably with timeouts.
If you really want to use your custom handler, you should check if you are in the right site context before calling WebUtil.ExecuteWebPage.

Related

ASP.NET Core - Identify if Request is Local [duplicate]

In the regular ASP.NET you could do this in a view to determine if the current request was from localhost:
HttpContext.Current.Request.IsLocal
But I can't find something similar in ASP.NET 6/Core/whatever it is meant to be called.
UPDATE: ASP.NET Core 2.0 has a method called Url.IsLocalUrl (see this Microsoft Docs).
I think this code will work, but I haven't been able to test it completely
var callingUrl = Request.Headers["Referer"].ToString();
var isLocal = Url.IsLocalUrl(callingUrl);
But see Will Dean's comment below about this approach:
Anyone thinking about using the 'updated' version which checks the Referrer header should bear in mind that headers are extremely easy to spoof, to a degree that doesn't apply to loopback IP addresses.
Original solution
I came across this looking for a solution to knowing if a request is local. Unfortunately ASP.NET version 1.1.0 does not have a IsLocal method on a connection. I found one solution on a web site called Strathweb but that is out of date too.
I have created my own IsLocal extension, and it seems to work, but I can't say I have tested it in all circumstances, but you are welcome to try it.
public static class IsLocalExtension
{
private const string NullIpAddress = "::1";
public static bool IsLocal(this HttpRequest req)
{
var connection = req.HttpContext.Connection;
if (connection.RemoteIpAddress.IsSet())
{
//We have a remote address set up
return connection.LocalIpAddress.IsSet()
//Is local is same as remote, then we are local
? connection.RemoteIpAddress.Equals(connection.LocalIpAddress)
//else we are remote if the remote IP address is not a loopback address
: IPAddress.IsLoopback(connection.RemoteIpAddress);
}
return true;
}
private static bool IsSet(this IPAddress address)
{
return address != null && address.ToString() != NullIpAddress;
}
}
You call it in a controller action from using the Request property, i.e.
public IActionResult YourAction()
{
var isLocal = Request.IsLocal();
//... your code here
}
I hope that helps someone.
At the time of writing HttpContext.Connection.IsLocal is now missing from .NET Core.
Other working solution checks only for a first loopback address (::1 or 127.0.0.1) which might not be adequate.
I find the solution below useful:
using Microsoft.AspNetCore.Http;
using System.Net;
namespace ApiHelpers.Filters
{
public static class HttpContextFilters
{
public static bool IsLocalRequest(HttpContext context)
{
if (context.Connection.RemoteIpAddress.Equals(context.Connection.LocalIpAddress))
{
return true;
}
if (IPAddress.IsLoopback(context.Connection.RemoteIpAddress))
{
return true;
}
return false;
}
}
}
And the example use case:
app.UseWhen(HttpContextFilters.IsLocalRequest, configuration => configuration.UseElmPage());
None of the above worked for me.
Url.IsLocalUrl works very different and I find it a bit useless:
For example, the following URLs are considered local:
/Views/Default/Index.html
~/Index.html
The following URLs are non-local:
../Index.html
http://www.contoso.com/
http://localhost/Index.html
HttpContext.Connection.IsLocal doesn't exist in .Net Core 2.2
Comparing ControllerContext.HttpContext.Connection.RemoteIpAddress and ControllerContext.HttpContext.Connection.LocalIpAddress also doesn't work in my test because I get "::1" for remote ip and "127.0.0.1" for local ip.
Finally, I used this piece:
IPAddress addr = System.Net.IPAddress.Parse( HttpContext.Connection.RemoteIpAddress.ToString() );
if (System.Net.IPAddress.IsLoopback(addr) )
{
//do something
}
Late to the party, but if I want to check IsLocal in razor views in .Net core 2.2+, I just do this:
#if (Context.Request.Host.Value.StartsWith("localhost"))
{
//do local stuff
}
UPDATE for ASP.NET Core 3.1
You can use this:
if (Request.Host.Host == "localhost") {// do something }
I would also mention that it may be useful to add the below clause to the end of your custom IsLocal() check
if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null)
{
return true;
}
This would account for the scenario where the site is being ran using the Microsoft.AspNetCore.TestHost and the site is being ran entirely locally in memory without an actual TCP/IP connection.
now its
HttpContext.Connection.IsLocal
and if you need to check that outside of a controller then you take a dependency on IHttpContextAccessor to get access to it.
Update based on comment:
HttpContext is intrinsically available in Views
#if (Context.Connection.IsLocal)
{
}

Losing Cookies and Session Variables during AuthorizeCore

I'm currently working on getting a test environment stood up (it is currently called DEV) and am experiencing some weird issues.
When you first come to the site, we have an agreement page. Hitting the "I Agree" button will force the user through an Action to check to see if they are a member of the site already or not. We do use a demo mode also, but that is not part of the issue.
The issue I'm currently experiencing is the following. Initially in the Action, we create a Cookie called "siteaccept". Once that is created, we determine if the site is in demo mode or not, then move on to getting the user (actual user or demo user). Once the user is found, we log their Id in a Cookie called "cntPOC", and also create a Session variable by the same name with the same data (original developers wrote much of this convoluted logic which I want to change before someone asks why keep a Session and Cookie). We then do a RedirectToAction to the Action to bring up the main page of the site.
Here is where the issue comes into play. The main page of the site's Action has a CustomAuthorizeAttribute decoration on it. In our CustomAuthorizeAttribute class, we have OnAuthorizion and AuthorizeCore being overrode. OnAuthorizion fires off first, however, it uses base.OnAuthorization. Once that is called, AuthorizeCore is called. In AuthorizeCore, we check for the "siteaccept" Cookie, followed by a check on the "cntPOC" Session variable. If both are there, we return true, otherwise false if either fails.
On not only my local environment but the DBA's, this works without a hitch. I see our Cookies and Session variable. However, on our DEV environment, both the Cookies and Session variable are missing. We have IE 11 configured to allow Cookies, yet we cannot get them once we leave the Action and proceed into the CustomAuthorizeAttribute.
I did find I can find the Cookie today if I check HttpContext.Current.Response instead of HttpContext.Current.Request, but that is the incorrect way to do it obviously.
Below is my code. I'm fairly certain since the code works on my local environment, it should be fine in our DEV environment. Also a quick note, our production environment does work, so the code obviously functions. It's a question now of why does the DEV environment not.
MainController.cs
[HttpPost]
public ActionResult Index(FormCollection frmCollection)
{
try
{
Response.Cookies.Remove("bracmisaccept");
HttpCookie cookie = new HttpCookie("bracmisaccept");
cookie.Value = "true";
Response.Cookies.Add(cookie);
...
//Demo Mode
var poc = new HttpCookie("cntPOC");
cookie.Value = "7578";
Response.Cookies.Add(poc);
Session["cntPOC"] = 7578;
return RedirectToAction("ApplicationSelection");
}
catch (Exception ex)
{
logger.LogError("Main Index", ex);
return PartialView(#"../Error/ExceptionHandling");
}
}
[CustomAuthorizeAttribute]
public ActionResult ApplicationSelection()
{
return View();
}
CustomAuthorizeAttribute.cs
public string RedirectUrl = "~/Main/SessionTimeout";
public string CookieExpiredRedirectUrl = "~/Main/Index";
public string AjaxRedirectUrl = "~/Error/AjaxError";
private bool _isAuthorized;
private bool _isCookieExpired;
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (HttpContext.Current.Request.Cookies["siteaccept"] == null)
{
_isAuthorized = false;
_isCookieExpired = true;
return false;
}
if (HttpContext.Current.Session["cntPOC"] == null)
{
_isAuthorized = false;
return false;
}
return true;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
if (!_isAuthorized)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.End();
}
else
{
if(_isCookieExpired)
filterContext.RequestContext.HttpContext.Response.Redirect(CookieExpiredRedirectUrl);
else
filterContext.RequestContext.HttpContext.Response.Redirect(RedirectUrl);
}
}
}
I'm fairly certain the code is fine, but I did read in a few articles that AuthorizeCore may or may not have the Cookies and Session variables at times. I just wanted to find out if I'm wasting my time with changing the code or if it's the box we have this site on. The server is super locked down, so yeah, kind of annoying...
Edit: I have yet to figure out how to fix this yet, however, I did find if I do a publish on this code, I can enter into the site properly. I still cannot run localhost to inspect the site, but a publish fixes a few minor issues of whether things will work on this site.

How to make form page invalid?

I am creating pages in asp.net. I created WebForm called Form.aspx. Now I want all request *.htm request to load this pages where I will do what I need (maybe not the best approach but it works as needed for me). So I created something like this:
routes.MapPageRoute(null, "{file}.htm", "~/Pages/Form.aspx");
routes.MapPageRoute(null, "{folder}/{file}.htm", "~/Pages/Form.aspx");
Now everything like http://example.com/whatever.htm or http://example.com/whatever/whatever.htm is redirected to my Form.aspx. But this Form.aspx doesn't have any meaning on it’s own. So following page is nonsense http://example.com/Pages/Form.aspx. How can I make it invalid? So it would show to me something like "Error HTTP 404.0 – Not Found". I want the same behaviour as if I would wrote "http://example.com/doesntexist.aspx". I don't want to do any redirection (only if no other option exists). I so far tried only something like this (which doesn't work):
routes.MapPageRoute(null, "Pages/Form.aspx", "~/doesntexist.aspx");
It doesn't do anything... Any help appreciated.
In Global.asax add this code:
...
protected void Application_BeginRequest(object sender, EventArgs e)
{
string requestPath = Request.RawUrl.Trim().ToLower();
HttpApplication app = sender as HttpApplication;
if (!IsLocalRequest(app.Context.Request))
{
if (requestPath.IndexOf("form.aspx") > 0)
{
throw new HttpException(404, "Error HTTP 404.0 – Not Found.");
}
}
}
// This method determines whether request came from the same IP address as the server.
public static bool IsLocalRequest(HttpRequest request)
{
return request.ServerVariables["LOCAL_ADDR"] == request.ServerVariables["REMOTE_ADDR"];
}
...

"Access Denied" error whilst programmatically activating a feature in SharePoint 2010

I am new to SharePoint so I am following some Microsoft Learning Guides. One exercise is to create a feature reciever to modify the Web.Config file.
I detect the feature being activated or deactivated and call the following routine with the appropriate flag.
void setProliferationFlag(bool status)
{
SPWebApplication webApp = SPWebApplication.Lookup(new Uri("http://SharePoint"));
try
{
SPWebConfigModification mySetting = null;
if (status)
{
mySetting = new SPWebConfigModification();
mySetting.Path = "configuration/appSettings";
mySetting.Name = "add [#key='preventProliferation'] [#value='1']";
mySetting.Sequence = 0;
mySetting.Owner = "Lab05Owner";
mySetting.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
mySetting.Value = "<add key='preventProliferation' value='1' />";
webApp.WebConfigModifications.Add(mySetting);
}
else
{
foreach (SPWebConfigModification modification in
webApp.WebConfigModifications)
{
if (modification.Owner == "Lab05Owner")
{
modification.Value = "<add key='preventProliferation' value='0' />";
}
}
}
webApp.Update();
webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
}
catch
{
}
The event receivers work fine. When I activate the feature this code is run, but when it reaches the "webApp.Update()" line it fails with an "Access Denied" error. No other detils on the error. I am not sure to what the access is denied.
I am running in my development environment on my laptop. This is a Sharepoint Server 2010 installation on Window 7.
Regards Tim
Most likely you will need administrative access. Look at the SPSecurity.RunWithElevatedPrivileges method which allows you to execute such actions within the system account's security context.
You will have to run the whole code elevated, that is including opening the SPWebApplication object. You method will then look like this:
void SetProliferationFlag(…)
{
SPSecurity.RunWithElevatedPrivileges(() =>
{
// … your code goes here …
});
}
Please also note, it's a very bad practice to have empty catch clauses in your code. Do always handle all exceptions, at least by logging them and rethrowing.

UrlRewriter+HttpModule+Session problem

I need to write a custom "UrlRewriter" using a HttpModule, in the moment of "rewriting" I need access to the Session and has followed the advice from another SO thread:
Can I access session state from an HTTPModule?
Everything works, except the RewritePath/Redirect part. I don't get any exceptions, but the browser takes forever to load. Is this really the best way to build a urlrewriter like this?
using System;
using System.Web;
using System.Web.SessionState;
using System.Diagnostics;
namespace MyCompany.Campaigns
{
public class CampaignRewriteModule : IHttpModule
{
public void Init(HttpApplication application)
{
application.PostAcquireRequestState += new EventHandler(Application_PostAcquireRequestState);
application.PostMapRequestHandler += new EventHandler(Application_PostMapRequestHandler);
}
void Application_PostMapRequestHandler(object source, EventArgs e)
{
HttpApplication app = (HttpApplication)source;
if (app.Context.Handler is IReadOnlySessionState || app.Context.Handler is IRequiresSessionState)
{
return;
}
app.Context.Handler = new MyHttpHandler(app.Context.Handler);
}
void Application_PostAcquireRequestState(object source, EventArgs e)
{
HttpApplication app = (HttpApplication)source;
MyHttpHandler resourceHttpHandler = HttpContext.Current.Handler as MyHttpHandler;
if (resourceHttpHandler != null)
{
HttpContext.Current.Handler = resourceHttpHandler.OriginalHandler;
}
Debug.Assert(app.Session != null);
string path = HttpUtils.Path();
if (!CampaignCodeMethods.IsValidCampaignCode(path)) return;
string domain = HttpUtils.Domain();
CampaignCode code = CampaignManager.RegisterCode(path, domain.Equals(Config.Instance.Domain.ToLower()) ? null : domain);
if (code != null)
{
//app.Context.RewritePath(code.CampaignCodePath.Path, false);
app.Context.Response.Redirect(code.CampaignCodePath.Path, true);
}
}
public void Dispose() { }
public class MyHttpHandler : IHttpHandler, IRequiresSessionState
{
internal readonly IHttpHandler OriginalHandler;
public MyHttpHandler(IHttpHandler originalHandler)
{
OriginalHandler = originalHandler;
}
public void ProcessRequest(HttpContext context)
{
throw new InvalidOperationException("MyHttpHandler cannot process requests.");
}
public bool IsReusable
{
get { return false; }
}
}
}
}
I think I know what it is. Your module is executed on ALL requests and assigns a handler that throws an error unless there is a valid campaign code (where a rewrite/redirect occurs).
But because this is not just for your "handler campaign code" url it is causing an error to be thrown, which is causing you to be redirected to your error page, which is being caught by the module, which is assigning the handler, which is throwing an error, which is redirecting... I think you get where I'm going ;)
Otherwise I'd try a few things:
Setup Fiddler and check for an infinite redirect loop
Put a breakpoint on app.Context.Response.Redirect - make sure your not in an infinite loop
Put a breakpoint on MyHttpHandler.ProcessRequest - make sure it's not being called and the exception swallowed
I wrote a simple URL rewriter module that did something similar. The url rewriting is done in BeginRequest by comparing the requested url to a list of known urls. If we find a mach we use HttpContext.RewritePath to change the requested url.
This appears to work well with no serious side effects.
I notice that you use Response.Redirect instead of Context.RewritePath. Using Redirect will cause the users browser to request a new page with the new url. Is this really what you want? The user will then see the new url in his browser. If this really is what you want you could use an alternative approach where you use a custom 404 page not found error handler to redirect the user to the appropriate page.
If you set up IIS to redirect all 404 errors to a new page, say Custom404.aspx, that you have set up. In this page you can check the requested url to see if the url should be rewritten. If it should you can simply set the Response.Status to "301 Moved Permanently" and write a header with the name "Location" and the new url as the value. If the url should not be rewritten you can just output the standard 404 page not found error.
This last approach works well, but as with your Response.Redirect approach the user will see the new url in his browser. Using Context.RewritePath allows you to serve a different page than the one requested.
Is your URL rewriter handling requests that aren't for an actual page? If they are, then I don't think you can access Session... the last URL rewriter that I had written was there to handle 404 errors, and I remember digging around and finding (somewhere, can't remember where) that you don't get access to Session if that request is not for an actual .aspx page.
I'm thinking the problem may be inside this block:
if (code != null)
{
//app.Context.RewritePath(code.CampaignCodePath.Path, false);
app.Context.Response.Redirect(code.CampaignCodePath.Path, true);
}
Try putting a breakpoint in the if statement and see if continually gets hit.
I think there should be a call to 'return' after you reset it to the original handler, else you will continually rewrite the path.
Thinking about it, that's probably why the page is loading forever! :)

Categories