SSL Links/URLs in MVC

June 11, 2008 9:06 PM

A couple days ago a reader sent me a question regarding how to use SSL with the MVC framework. Specifically the reader wanted to know the easiest way to make an Ajax call to a HTTPS page from a non HTTPS page. The tricky part here is to do so without hard-coding any URLs (as one of the best practices of the MVC framework is that views and controllers should be divorced from the routes that access them).

Currently an ActionLink like:

   1: <% =Html.ActionLink( "Test", "About", "Home" ) %>

... outputs the following hyperlink:

   1: <a href="/Home/About">Test</a>

I've created a few extension methods that enable you to fully-qualify these URLs and change the protocol to HTTPS at run-time. Using the extension method "ToSslLink" like so:

   1: <% =Html.ActionLink( "Test", "About", "Home" ).ToSslLink() %>

... now outputs a hyperlink with protocol, server, port, etc:

   1: <a href="https://localhost:57626/Home/About">Test</a>

If you're using Ajax, however, you'll be more interested in getting to the URL directly rather than building an entire hyperlink. In that case you can use "ToSslUrl":

   1: <% =Url.Action( "About", "Home" ).ToSslUrl() %>

... to output this:

   1: https://localhost:57626/Home/About

Here is the code for the extension methods I've created, hopefully some of you will find this useful:

   1: using System.Text.RegularExpressions;
   2: using System.Web;
   3:  
   4: namespace SquaredRoot.Mvc.Extensions.Ssl
   5: {
   6:     /// <summary>
   7:     /// Provides helper extensions for turning strings into fully-qualified and SSL-enabled Urls.
   8:     /// </summary>
   9:     public static class UrlStringExtensions
  10:     {
  11:         /// <summary>
  12:         /// Takes a relative or absolute url and returns the fully-qualified url path.
  13:         /// </summary>
  14:         /// <param name="text">The url to make fully-qualified. Ex: Home/About</param>
  15:         /// <returns>The absolute url plus protocol, server, & port. Ex: http://localhost:1234/Home/About</returns>
  16:         public static string ToFullyQualifiedUrl( this string text )
  17:         {
  18:             //### the VirtualPathUtility doesn"t handle querystrings, so we break the original url up
  19:             var oldUrl = text;
  20:             var oldUrlArray = ( oldUrl.Contains( "?" ) ? oldUrl.Split( '?' ) : new[]{ oldUrl, "" } );
  21:  
  22:             //### we"ll use the Request.Url object to recreate the current server request"s base url
  23:             //### requestUri.AbsoluteUri = "http://domain.com:1234/Home/Index"
  24:             //### requestUri.LocalPath = "/Home/Index"
  25:             //### subtract the two and you get "http://domain.com:1234", which is urlBase
  26:             var requestUri = HttpContext.Current.Request.Url;
  27:             //### fix for Mike Hadlow's reported issue regarding extraneous link elements when a querystring is present
  28:             //var urlBase = requestUri.AbsoluteUri.Substring( 0, requestUri.AbsoluteUri.Length - requestUri.LocalPath.Length );
  29:             var localPathAndQuery = requestUri.LocalPath + requestUri.Query;
  30:             var urlBase = requestUri.AbsoluteUri.Substring( 0, requestUri.AbsoluteUri.Length - localPathAndQuery.Length ); 
  31:  
  32:             //### convert the request url into an absolute path, then reappend the querystring, if one was specified
  33:             var newUrl = VirtualPathUtility.ToAbsolute( oldUrlArray[0] );
  34:             if( !string.IsNullOrEmpty( oldUrlArray[1] ) )
  35:                 newUrl += "?" + oldUrlArray[1];
  36:  
  37:             //### combine the old url base (protocol + server + port) with the new local path
  38:             return urlBase + newUrl;
  39:         }
  40:  
  41:         /// <summary>
  42:         /// Looks for Html links in the passed string and turns each relative or absolute url and returns the fully-qualified url path.
  43:         /// </summary>
  44:         /// <param name="text">The url to make fully-qualified. Ex: <a href="Home/About">Blah</a></param>
  45:         /// <returns>The absolute url plus protocol, server, & port. Ex: <a href="http://localhost:1234/Home/About">Blah</a></returns>
  46:         public static string ToFullyQualifiedLink( this string text )
  47:         {
  48:             var regex = new Regex(
  49:                 "(?<Before><a.*href=\")(?!http)(?<Url>.*?)(?<After>\".+>)",
  50:                 RegexOptions.Multiline | RegexOptions.IgnoreCase
  51:                 );
  52:  
  53:             return regex.Replace( text, ( Match m ) =>
  54:                                         m.Groups["Before"].Value +
  55:                                         ToFullyQualifiedUrl( m.Groups["Url"].Value ) +
  56:                                         m.Groups["After"].Value
  57:                 );
  58:         }
  59:  
  60:         /// <summary>
  61:         /// Takes a relative or absolute url and returns the fully-qualified url path using the Https protocol.
  62:         /// </summary>
  63:         /// <param name="text">The url to make fully-qualified. Ex: Home/About</param>
  64:         /// <returns>The absolute url plus server, & port using the Https protocol. Ex: https://localhost:1234/Home/About</returns>
  65:         public static string ToSslUrl( this string text )
  66:         {
  67:             return ToFullyQualifiedUrl( text ).Replace( "http:", "https:" );
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Looks for Html links in the passed string and turns each relative or absolute url into a fully-qualified url path using the Https protocol.
  72:         /// </summary>
  73:         /// <param name="text">The url to make fully-qualified. Ex: <a href="Home/About">Blah</a></param>
  74:         /// <returns>The absolute url plus server, & port using the Https protocol. Ex: <a href="https://localhost:1234/Home/About">Blah</a></returns>
  75:         public static string ToSslLink( this string text )
  76:         {
  77:             return ToFullyQualifiedLink( text ).Replace( "http:", "https:" );
  78:         }
  79:     }
  80: }

One important missing element from all is this posting to a form via HTTPS from a non-HTTPS page. You can certainly create your form tags manually and populate the action attribute using Url.Action(...).ToSslUrl(), but you cannot try and combine these extension methods with the Html.Form(...) helper that is most commonly used inside a using statement.

Note: The above code was updated on July 7th, 2008 to fix a bug reported by Mike Hadlow. Thanks Mike!

Tags: , , ,
Categories: MVC
Actions: E-mail | Permalink | Comments (9) RSS Feed for this post's comments.

Comments

6/12/2008 8:35 AM #

tgmdbm

It's unfortunate that we need to use reflection but here you go...


public static SimpleForm ToSSlForm(this SimpleForm form)
{
var type = form.GetType();
var field = type.GetField( "_formAction", BindingFlags.NonPublic );
string Url = field.GetValue( form );
field.SetValue( form, Url.ToSslUrl() );
return form;
}

It would be nice to just say
form.Action = form.Action.ToSslUrl()
but that's just an oversight that hopefully will get resolved.

tgmdbm gb

6/12/2008 10:13 AM #

Troy Goode

Thanks for the example, tgmdbm, but I have a question about how one would use this. The .Form extension method of the HtmlHelper class creates a new instance of SimpleForm and then immediately calls WriteStartTag(), which means this:

using( Html.Form(...).ToSslForm() ){...}

... would not work because the start tag has already been written to the response stream before ToSslForm is called.

It appears to me that to use the ToSslForm extension method you would have to manually create an instance of SimpleForm, passing the HttpContext, RouteValueDictionary, action, and method directly. And if you're doing that you don't actually gain anything with the ToSslForm extension because you're already supplying the action directly.

Am I missing something?

Troy Goode us

6/12/2008 10:18 AM #

tgmdbm

*blush*

You're right. I wasn't thinking. It would need to be Html.SslForm(...)

tgmdbm gb

6/12/2008 11:45 AM #

Troy Goode

Yeah, I considered making extension methods for Html.SslForm, but there are a lot of overloads to implement and as I don't currently have a need for that functionality I got lazy and copped out.

There is always:


<form method="post" action="<% =Url.Action( "About", "Home" ).ToSslUrl() %>">
...
</form>

Troy Goode us

6/12/2008 3:28 PM #

Joel Cochran

It seems that the ActionLink and Url helpers need to include options like Protocol and IsFullyQualified.

Joel Cochran us

6/16/2008 5:12 AM #

翻译公司

SSL is also difficult to me and my friends. Thanks! I am a software student in China.

翻译公司 cn

7/6/2008 4:09 AM #

Mike Hadlow

Thanks for this, I've been using it in my open source eCommerce application, suteki shop (http://code.google.com/p/sutekishop/). It's all working great except for one small bug. If the current URL has a query string when you call ToSslUrl(), the method ToFullyQualifiedUrl doesn't correctly calculate the length of urlBase variable so you get repeated path in the result. Something like this:

jtg.sutekishop.co.uk/.../PlaceOrder

To fix it just include the query string in the urlBase calculation, replace this code in ToFullyQualifiedUrl:

var urlBase = requestUri.AbsoluteUri.Substring(0, requestUri.AbsoluteUri.Length - requestUri.LocalPath.Length);

with this:

var localPathAndQuery = requestUri.LocalPath + requestUri.Query;
var urlBase = requestUri.AbsoluteUri.Substring(0, requestUri.AbsoluteUri.Length - localPathAndQuery.Length);

Mike Hadlow gb

7/7/2008 11:35 PM #

Troy Goode

Interesting. Thanks Mike. I honestly haven't had a chance to use the code (I currently don't have any need and really only wrote it to help out a reader), so I'm not terribly surprised to find out it has some kinks.

Let me try to reproduce the issue and solution and then I'll update the post.

Troy Goode us

7/7/2008 11:47 PM #

Troy Goode

Everything checked out and I went ahead and updated the code in the post. Thanks a ton, Mike!

Troy Goode us

Add Comment


(Will show your Gravatar icon)  

  Country flag

biuquote
  • Comment
  • Preview
Loading




Troy Goode

Troy Goode
Microsoft Certified Professional Developer
AddThis Feed Button

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in  anyway.

© Copyright 2008

Colophon

Powered by:
BlogEngine.NET 1.4
Template:
Designs by Darren
Header Font:
Stamper
Syntax Highlighting:
WLW Code Snippet Plugin