SSL Links/URLs in MVC
// June 11th, 2008 // MVC
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!




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.
*blush*
You’re right. I wasn’t thinking. It would need to be Html.SslForm(…)
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:
[quote]using( Html.Form(…).ToSslForm() ){…}[/quote]
… 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?
It seems that the ActionLink and Url helpers need to include options like Protocol and IsFullyQualified.
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:
[quote]
[/quote]
SSL is also difficult to me and my friends. Thanks! I am a software student in China.
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:
https://jtg.sutekishop.co.uk/shop/Order//shop/Order/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);
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.
Everything checked out and I went ahead and updated the code in the post. Thanks a ton, Mike!
Please I need your help I’ve just copied the source code on this page but it does not work for me. Here is the error I got (’string’ does not contain a definition for ‘ToSslLink’ and no extension method ‘ToSslLink’ accepting a first argument of type ’string’ could be found (are you missing a using directive or an assembly reference?)). Any idea
Thanks
Borris
Troy,
I like your ToSslUrl() implementation. It seems once you call it all subsequent requests use the https. I would like to call ToSslUrl() for my Login and Checkout links, but otherwise, allow just http. What do you think the best way is to accomplish that? Do I need a ToNonSslUrl()? And how would I do this in the controller (i.e. after a successful login)?
Thanks,
Todd
I’m unsure: if you submit an ajax post even to a correctly qualified https url from a http (non-secure) page, isn’t it still unsafe as the crypt tokens are not established prior to the post? ie. the post data will NOT be encrypted?
If the fully qualified URL contains a port, you would want to strip the port as well as changing http to https, because http and https cannot run on the same port. Most likely SSL is running on port 443.
Also, when replacing “http:” with “https:” you would only want to replace the first instance, due to the fact that it is possible “http:” could appear in the querystring.
[...] SSL Links/URLs in MVC | SquaredRoot (tags: mvc asp.net ssl aspnetmvc howto httpmodule url extension) [...]