Combined SSL HttpModule for SharePoint Webs and Pages
UPDATE (03/08/2009): See updated post meant to handle an issue where entire page content wasn't being transmitted over SSL.
I'm currently working on a .com SharePoint site where access to several sub-sites is restricted to logged in users. The team also implemented custom login and registration pages for these sites as layouts pages.
We have a requirement that these sub-sites and the layouts pages both use SSL.
I came across two very helpful blog posts that addressed each scenario individually:
With very little tweaking, each of these handles the particular situation it was designed for very well. However, I basically needed a combination of these two approaches.
If I tried enabling both HttpModules on my SharePoint web application, one scenario would work, but not the other. So I decided to combine both approaches into a single HttpModule:
1: public class SSLHttpModule : IHttpModule
2: { 3: private HttpApplication _application;
4:
5: private String _baseURLNoSSL;
6: private String _baseURLWithSSL;
7: private String _sslPagesList;
8:
9: private List<String> _sslPages = new List<string>();
10:
11: const String BASEURL_NOSSL = "BaseUrlNoSSL";
12: const String BASEURL_SSL = "BaseUrlSSL";
13: const String SITE_REQUIRES_SSL_PROPERTY = "requiressl";
14: const String SSL_LAYOUTS_PAGES = "SSLLayoutsPages";
1:
2: public void Init(HttpApplication application)
3: { 4: if (ConfigurationManager.AppSettings[BASEURL_NOSSL] != null)
5: _baseURLNoSSL = ConfigurationManager.AppSettings[BASEURL_NOSSL];
6: else
7: return;
8: if (ConfigurationManager.AppSettings[BASEURL_SSL] != null)
9: _baseURLWithSSL = ConfigurationManager.AppSettings[BASEURL_SSL];
10: else
11: return;
12: if (ConfigurationManager.AppSettings[SSL_LAYOUTS_PAGES] != null)
13: _sslPagesList = ConfigurationManager.AppSettings[SSL_LAYOUTS_PAGES];
14: else
15: return;
16:
17: // Create a List<String> of Layouts pages requiring SSL
18: //
19: char[] separator; separator = new char[] { ',' }; 20: string[] pages; pages = _sslPagesList.Split(separator);
21: _sslPages.AddRange(pages);
22:
23: application.PreRequestHandlerExecute += new EventHandler(PreRequest);
24: _application = application;
25: }
There are three application settings (that live in web.config) that make the whole thing work:
- BaseUrlNoSSL - the url of your site when you're not using SSL
- BaseUrlSSL - the url of your site when you're using SSL
- I use an Alternate Access Mapping to do this
- SSLLayoutsPages - a comma-separated list of layouts pages which should be secured by SSL
- Nothing's stopping you from modifying this code to make it work with any page, not just layouts pages.
The following listing shows the PreRequest event of the HttpModule. What I'm doing differently here is handling the case for layouts pages first, and then checking if the SPWeb needs SSL.
1: public void PreRequest(object sender, EventArgs e)
2: { 3: HttpContext ctx = null;
4: SPContext spContext = null;
5:
6: try
7: { 8: ctx = HttpContext.Current;
9: spContext = SPContext.Current;
10: if (spContext != null)
11: { 12: if (spContext.Web != null)
13: { 14: string pathAndQuery = ctx.Request.Url.PathAndQuery;
15:
16: if (pathAndQuery.ToLower().Contains("_layouts")) 17: { 18: bool pageRequiresSSL = false;
19:
20: foreach (var page in _sslPages)
21: { 22: if (pathAndQuery.ToLower().Contains(page.ToLower()))
23: { 24: pageRequiresSSL = true;
25: break;
26: }
27: }
28:
29: if (pageRequiresSSL)
30: { 31: if (HttpContext.Current.Request.Url.Scheme.ToString() == "http")
32: { 33: string url = HttpContext.Current.Request.Url.ToString();
34: url = url.Replace("http://", "https://"); 35: HttpContext.Current.Response.Redirect(url);
36: }
37: }
38: else
39: { 40: if (HttpContext.Current.Request.Url.Scheme.ToString() == "https")
41: { 42: string url = HttpContext.Current.Request.Url.ToString();
43: url = url.Replace("https://", "http://"); 44: HttpContext.Current.Response.Redirect(url);
45: }
46: }
47: }
48: else
49: { 50: bool siteRequiresSSL = false;
51: if (spContext.Web.Properties.ContainsKey(SITE_REQUIRES_SSL_PROPERTY))
52: { 53: siteRequiresSSL = (spContext.Web.Properties
54: [SITE_REQUIRES_SSL_PROPERTY].ToString()).ToLower()
55: == Boolean.TrueString.ToLower();
56: }
57:
58: if (siteRequiresSSL & !ctx.Request.IsSecureConnection)
59: { 60: if (_baseURLWithSSL != null)
61: ctx.Response.Redirect(_baseURLWithSSL + pathAndQuery);
62: else
63: ctx.Response.Redirect(String.Concat("https://", 64: ctx.Request.Url.Host, pathAndQuery));
65: return;
66: }
67: if (!siteRequiresSSL & ctx.Request.IsSecureConnection)
68: { 69: if (_baseURLNoSSL != null)
70: ctx.Response.Redirect(_baseURLNoSSL + pathAndQuery);
71: else
72: ctx.Response.Redirect(String.Concat("http://", 73: ctx.Request.Url.Host, pathAndQuery));
74: return;
75: }
76: }
77: }
78: }
79:
80: }
81: catch (Exception) { } 82: }
Some notes about this:
- I provisioned the RequireSSL property of each SPWeb as part of our provisioning process in a custom site definition.
- We provisioned all the required web.config modifications in a feature receiver, definitely a much more elegant way than manually modifying your web.config file