Remote Require HTTPS MVC 5

2019-01-26 02:05发布

问题:

I have the following attribute to make sure that the remote site page opens in https mode.

public class RemoteRequireHttpsAttribute : RequireHttpsAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentException("Filter Context");
            }

            if (filterContext != null && filterContext.HttpContext != null)
            {
                if (filterContext.HttpContext.Request.IsLocal)
                {
                    return;
                }
                else
                {
                    string val = ConfigurationManager.AppSettings["RequireSSL"].Trim();
                    bool requireSsl = bool.Parse(val);
                    if (!requireSsl)
                    {
                        return;
                    }
                }
            }

            base.OnAuthorization(filterContext);
        }
    }

Local development now work normal since i don't want it to open in https mode.

Dev site opens the page in https mode - no issues here (single node).

Where as the production (load balanced - 2 nodes) site that i am currently setting up is giving me following error. Please note that dev and prod sites have the same setings and web.config

The page isn't redirecting properly

Firefox has detected that the server is redirecting the request for this address in a way that will never complete.

This problem can sometimes be caused by disabling or refusing to accept cookies.

Dev site url is like http://dev.datalab.something.org

Prod site url is like http://datalab.something.org

And here is the call

[RemoteRequireHttps]
public ActionResult Index(string returnUrl, string error)

What am i missing here?

Update 1: My admin has confirmed that the SSL termination has been setup at the lad balancer evel. I have looked at the iis site setup and i don't see https bindings. I only see http binding. Does he need to setup https bindings as well?

Update 2: @AlexeiLevenkov pointed me to the right direction and this post had the code that I utilized and it is working. MOVED the code into a separate answer.

回答1:

Your site is behind load balancer that does SSL termination - so as result all incoming traffic to your site is HTTP irrespective what user sees. This causes your code to always try to redirect to HTTPS version and hence infinite loop.

Options to fix:

  • Usually load balancer that does SSL termination will forward original IP/protocol via custom headers. x-forwarded-proto and x-forwarded-for are common ones to be used for this purpose. You may need to check with network admins if these headers used or some additional configuration is needed
  • Alternatively you can turn off SSL termination, but it will put additional load on your server.
  • One can also configure load balancer to talk to server with the same protocol as incoming request.

How to investigate such issue:

  • Look at http debugger (like Fiddler) to see if you are getting 30x redirects requests in a loop. If no redirects - likely code is wrong.
  • If you see repeated redirects it likely means site does not see actual request information - could be protocol, path cookies missing.
  • To continue investigation see what devices are between user and server (CDN, proxies, load balancer,...) - each have good chance to loose some date or transform protocols.


回答2:

Not that I am against writing nice custom attributes, would it not make sense to perhaps perform the redirect in the web.config and use transformations available for the web.config to change the enabled value below from false to true for production deploys?

<rewrite>
  <rules>
    <rule name="SSL_ENABLED" enabled="false" stopProcessing="true">
      <match url="(.*)" />
      <conditions>
        <add input="{HTTPS}" pattern="^OFF$" />
      </conditions>
      <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
    </rule>
  </rules>
</rewrite>


回答3:

Moving the fix into a separate answer as noted by @AlexeiLevenkov.

public class RemoteRequireHttpsAttribute : RequireHttpsAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentException("Filter Context");
            }

            if(filterContext.HttpContext != null)
            {
                if (filterContext.HttpContext.Request.IsSecureConnection)
                {
                    return;
                }

                var currentUrl = filterContext.HttpContext.Request.Url;
                if (currentUrl.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.CurrentCultureIgnoreCase))
                {
                    return;
                }

                if (string.Equals(filterContext.HttpContext.Request.Headers["X-Forwarded-Proto"], "https", StringComparison.InvariantCultureIgnoreCase))
                {
                    return;
                }

                if (filterContext.HttpContext.Request.IsLocal)
                {
                    return;
                }

                var val = ConfigurationManager.AppSettings["RequireSSL"].Trim();
                var requireSsl = bool.Parse(val);
                if (!requireSsl)
                {
                    return;
                }
            }

            base.OnAuthorization(filterContext);
        }
    }

and i have updated the ExitHttps attribute as well. This was having the similar issues...

public class ExitHttpsAttribute : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentException("Filter Context");
            }

            if (filterContext.HttpContext == null)
            {
                return;
            }

            var isSecure = filterContext.HttpContext.Request.IsSecureConnection;

            var currentUrl = filterContext.HttpContext.Request.Url;
            if (!isSecure && currentUrl.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.CurrentCultureIgnoreCase))
            {
                isSecure = true;
            }

            if (!isSecure && string.Equals(filterContext.HttpContext.Request.Headers["X-Forwarded-Proto"], "https", StringComparison.InvariantCultureIgnoreCase))
            {
                isSecure = true;
            }

            if (isSecure)
            {
                //in these cases keep https
                // abort if a [RequireHttps] attribute is applied to controller or action
                if (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof (RequireHttpsAttribute), true).Length > 0)
                {
                    isSecure = false;
                }

                if (isSecure && filterContext.ActionDescriptor.GetCustomAttributes(typeof (RequireHttpsAttribute), true).Length > 0)
                {
                    isSecure = false;
                }

                // abort if a [RetainHttps] attribute is applied to controller or action
                if (isSecure && filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof (RetainHttpsAttribute), true).Length > 0)
                {
                    isSecure = false;
                }

                if (isSecure && filterContext.ActionDescriptor.GetCustomAttributes(typeof (RetainHttpsAttribute), true).Length > 0)
                {
                    isSecure = false;
                }

                // abort if it's not a GET request - we don't want to be redirecting on a form post
                if (isSecure && !String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
                {
                    isSecure = false;
                }
            }

            if (!isSecure)
            {
                return;
            }

            // redirect to HTTP
            var url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
            filterContext.Result = new RedirectResult(url);
        }
    }