diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Commands/SecureSocketsLayersCommand.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Commands/SecureSocketsLayersCommand.cs new file mode 100644 index 000000000..b580b6cb0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Commands/SecureSocketsLayersCommand.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Orchard.Commands; +using Orchard.ContentManagement; +using Orchard.SecureSocketsLayer.Models; + +namespace Orchard.SecureSocketsLayer.Commands { + public class SecureSocketsLayersCommand : DefaultOrchardCommandHandler { + private readonly IOrchardServices _services; + + public SecureSocketsLayersCommand(IOrchardServices services) { + _services = services; + } + + [OrchardSwitch] + public bool SecureEverything { get; set; } + [OrchardSwitch] + public bool CustomEnabled { get; set; } + [OrchardSwitch] + public string Urls { get; set; } + [OrchardSwitch] + public string SecureHostName { get; set; } + [OrchardSwitch] + public string InsecureHostName { get; set; } + + [CommandName("site setting set ssl")] + [CommandHelp("site setting set ssl /SecureEverything:true /CustomEnabled:true /Urls: /SecureHostName:domain.com /InsecureHostName:secure.domain.com\r\n" + + "\tSet the 'SSL' site settings. Urls example: /Urls:\"'mysite.com/a','mysite.com/b'\"")] + [OrchardSwitches("SecureEverything,CustomEnabled,Urls,SecureHostName,InsecureHostName")] + public void SetSSLInfo() { + var settings = _services.WorkContext.CurrentSite.As(); + if (settings == null) { + return; + } + + if (!string.IsNullOrWhiteSpace(Urls)) { + var comma = false; + var urlList = new List(); + try { + Urls = Urls.Trim(); + while (Urls.Length != 0) { + var first = Urls[0]; + if (first == ',' && comma) { + Urls = Urls.Substring(1); + comma = false; + } + else if (first == '\'' && !comma) { + int end = Urls.IndexOf('\'', 1); + if (end == -1) { + throw new ArgumentException("Invalid Urls"); + } + urlList.Add(Urls.Substring(1, end - 1)); + Urls = Urls.Substring(end + 1); + comma = true; + } + else { + throw new ArgumentException("Invalid Urls"); + } + } + if (!comma) + throw new ArgumentException("Invalid Urls"); + Urls = string.Join("\r\n", urlList); + } + catch(ArgumentException) { + Context.Output.WriteLine(T("'Urls' site setting invalid")); + return; + } + } + + if (string.IsNullOrWhiteSpace(Urls)) { + Urls = null; + } + + settings.SecureEverything = SecureEverything; + settings.CustomEnabled = CustomEnabled; + settings.Urls = Urls; + settings.SecureHostName = SecureHostName; + settings.InsecureHostName = InsecureHostName; + + Context.Output.WriteLine(T("'Secure Everything' site setting set to '{0}'", SecureEverything)); + Context.Output.WriteLine(T("'Custom Enabled' site setting set to '{0}'", CustomEnabled)); + Context.Output.WriteLine(T("'Urls' site setting set to '{0}'", Urls)); + Context.Output.WriteLine(T("'Secure Host Name' site setting set to '{0}'", SecureHostName)); + Context.Output.WriteLine(T("'Insecure Host Name' site setting set to '{0}'", InsecureHostName)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Drivers/SslSettingsPartDriver.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Drivers/SslSettingsPartDriver.cs new file mode 100644 index 000000000..323650d77 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Drivers/SslSettingsPartDriver.cs @@ -0,0 +1,58 @@ +using Orchard.Caching; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.ContentManagement.Handlers; +using Orchard.Localization; +using Orchard.SecureSocketsLayer.Models; + +namespace Orchard.SecureSocketsLayer.Drivers { + public class SslSettingsPartDriver : ContentPartDriver { + private readonly ISignals _signals; + private const string TemplateName = "Parts/SecureSocketsLayer.Settings"; + + public SslSettingsPartDriver(ISignals signals) { + _signals = signals; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + protected override string Prefix { + get { return "SslSettings"; } + } + + protected override DriverResult Editor(SslSettingsPart part, dynamic shapeHelper) { + return ContentShape("Parts_SslSettings_Edit", + () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix)) + .OnGroup("Ssl"); + } + + protected override DriverResult Editor(SslSettingsPart part, IUpdateModel updater, dynamic shapeHelper) { + if (updater.TryUpdateModel(part, Prefix, null, null)) { + _signals.Trigger(SslSettingsPart.CacheKey); + } + + return Editor(part, shapeHelper); + } + + protected override void Importing(SslSettingsPart part, ImportContentContext context) { + var elementName = part.PartDefinition.Name; + part.SecureEverything = bool.Parse(context.Attribute(elementName, "SecureEverything") ?? "true"); + part.CustomEnabled = bool.Parse(context.Attribute(elementName, "CustomEnabled") ?? "false"); + part.Urls = context.Attribute(elementName, "Urls") ?? ""; + part.InsecureHostName = context.Attribute(elementName, "InsecureHostName") ?? ""; + part.SecureHostName = context.Attribute(elementName, "SecureHostName") ?? ""; + + _signals.Trigger(SslSettingsPart.CacheKey); + } + + protected override void Exporting(SslSettingsPart part, ExportContentContext context) { + var el = context.Element(part.PartDefinition.Name); + el.SetAttributeValue("SecureEverything", part.SecureEverything); + el.SetAttributeValue("CustomEnabled", part.CustomEnabled); + el.SetAttributeValue("Urls", part.Urls); + el.SetAttributeValue("InsecureHostName", part.InsecureHostName); + el.SetAttributeValue("SecureHostName", part.SecureHostName); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Filters/SecureSocketsLayersFilter.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Filters/SecureSocketsLayersFilter.cs new file mode 100644 index 000000000..0c976c5e5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Filters/SecureSocketsLayersFilter.cs @@ -0,0 +1,58 @@ +using System.Collections.Specialized; +using System.Web.Mvc; +using Orchard.Mvc.Filters; +using Orchard.SecureSocketsLayer.Services; + +namespace Orchard.SecureSocketsLayer.Filters { + public class SecureSocketsLayersFilter : FilterProvider, IActionFilter { + private readonly ISecureSocketsLayerService _sslService; + + public SecureSocketsLayersFilter(ISecureSocketsLayerService sslService) { + _sslService = sslService; + } + + public void OnActionExecuted(ActionExecutedContext filterContext) {} + + public void OnActionExecuting(ActionExecutingContext filterContext) { + var user = filterContext.HttpContext.User; + var secure = + (user != null && user.Identity.IsAuthenticated) || + _sslService.ShouldBeSecure(filterContext); + + var request = filterContext.HttpContext.Request; + + // redirect to a secured connection ? + if (secure && !request.IsSecureConnection) { + var secureActionUrl = AppendQueryString( + request.QueryString, + _sslService.SecureActionUrl( + filterContext.ActionDescriptor.ActionName, + filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, + filterContext.RequestContext.RouteData.Values)); + + filterContext.Result = new RedirectResult(secureActionUrl); + return; + } + + // non auth page on a secure canal + // nb: needed as the ReturnUrl for LogOn doesn't force the scheme to http, and reuses the current one + if (!secure && request.IsSecureConnection) { + var insecureActionUrl = AppendQueryString( + request.QueryString, + _sslService.InsecureActionUrl( + filterContext.ActionDescriptor.ActionName, + filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, + filterContext.RequestContext.RouteData.Values)); + + filterContext.Result = new RedirectResult(insecureActionUrl); + } + } + + private static string AppendQueryString(NameValueCollection queryString, string url) { + if (queryString.Count > 0) { + url += '?' + queryString.ToString(); + } + return url; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Handlers/SslSettingsPartHandler.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Handlers/SslSettingsPartHandler.cs new file mode 100644 index 000000000..8d9e08d07 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Handlers/SslSettingsPartHandler.cs @@ -0,0 +1,26 @@ +using Orchard.ContentManagement; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; +using Orchard.Localization; +using Orchard.SecureSocketsLayer.Models; + +namespace Orchard.SecureSocketsLayer.Handlers { + public class SslSettingsPartHandler : ContentHandler { + public SslSettingsPartHandler() { + T = NullLocalizer.Instance; + Filters.Add(new ActivatingFilter("Site")); + } + + public Localizer T { get; set; } + + protected override void GetItemMetadata(GetContentItemMetadataContext context) { + if (context.ContentItem.ContentType != "Site") + return; + base.GetItemMetadata(context); + context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("SSL")) { + Id = "Ssl", + Position = "2" + }); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Models/SslSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Models/SslSettingsPart.cs new file mode 100644 index 000000000..842610534 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Models/SslSettingsPart.cs @@ -0,0 +1,49 @@ +using System; +using Orchard.ContentManagement; +using Orchard.ContentManagement.FieldStorage.InfosetStorage; + +namespace Orchard.SecureSocketsLayer.Models { + internal class SslSettings { + public string Urls { get; set; } + public bool SecureEverything { get; set; } + public bool CustomEnabled { get; set; } + public string SecureHostName { get; set; } + public string InsecureHostName { get; set; } + } + + public class SslSettingsPart : ContentPart { + public const string CacheKey = "SslSettingsPart"; + + public string Urls + { + get { return this.As().Get("Urls"); } + set { this.As().Set("Urls", value); } + } + + public bool SecureEverything { + get { + var attributeValue = this.As().Get("SecureEverything"); + return !String.IsNullOrWhiteSpace(attributeValue) && Convert.ToBoolean(attributeValue); + } + set { this.As().Set("SecureEverything", value.ToString()); } + } + + public bool CustomEnabled { + get { + var attributeValue = this.As().Get("CustomEnabled"); + return !String.IsNullOrWhiteSpace(attributeValue) && Convert.ToBoolean(attributeValue); + } + set { this.As().Set("CustomEnabled", value.ToString()); } + } + + public string SecureHostName { + get { return this.As().Get("SecureHostName"); } + set { this.As().Set("SecureHostName", value); } + } + + public string InsecureHostName { + get { return this.As().Get("InsecureHostName"); } + set { this.As().Set("InsecureHostName", value); } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Module.txt b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Module.txt new file mode 100644 index 000000000..0bc064e97 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Module.txt @@ -0,0 +1,11 @@ +Name: Secure Sockets Layer +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.7.1 +OrchardVersion: 1.7.1 +Description: This module will ensure SSL is used when accessing specific parts of the website like the dashboard, authentication pages or custom pages. +FeatureName: Secure Sockets Layer +Category: Security +FeatureDescription: Use SSL for specific parts of the website +Dependencies: Orchard.Users diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj new file mode 100644 index 000000000..8224c10a0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Orchard.SecureSocketsLayer.csproj @@ -0,0 +1,139 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {36B82383-D69E-4897-A24A-648BABDF80EC} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.SecureSocketsLayer + Orchard.SecureSocketsLayer + v4.0 + false + + + 4.0 + + + false + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + 3.5 + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Placement.info b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Placement.info new file mode 100644 index 000000000..d0034ee93 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Placement.info @@ -0,0 +1,3 @@ + + + diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9ed336f14 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.SecureSocketsLayer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("285775cf-fa49-4030-8a30-7872120d6574")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Services/ISecureSocketsLayerService.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Services/ISecureSocketsLayerService.cs new file mode 100644 index 000000000..0ed377816 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Services/ISecureSocketsLayerService.cs @@ -0,0 +1,16 @@ +using System.Web.Mvc; +using System.Web.Routing; + +namespace Orchard.SecureSocketsLayer.Services { + public interface ISecureSocketsLayerService : IDependency { + bool ShouldBeSecure(string actionName, string controllerName, RouteValueDictionary routeValues); + bool ShouldBeSecure(RequestContext requestContext); + bool ShouldBeSecure(ActionExecutingContext actionContext); + string InsecureActionUrl(string actionName, string controllerName); + string InsecureActionUrl(string actionName, string controllerName, object routeValues); + string InsecureActionUrl(string actionName, string controllerName, RouteValueDictionary routeValues); + string SecureActionUrl(string actionName, string controllerName); + string SecureActionUrl(string actionName, string controllerName, object routeValues); + string SecureActionUrl(string actionName, string controllerName, RouteValueDictionary routeValues); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Services/SecureSocketsLayerService.cs b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Services/SecureSocketsLayerService.cs new file mode 100644 index 000000000..0993e6b37 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Services/SecureSocketsLayerService.cs @@ -0,0 +1,246 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.Caching; +using Orchard.ContentManagement; +using Orchard.SecureSocketsLayer.Models; +using Orchard.UI.Admin; + +namespace Orchard.SecureSocketsLayer.Services { + public class SecureSocketsLayerService : ISecureSocketsLayerService { + private readonly IWorkContextAccessor _workContextAccessor; + private readonly ICacheManager _cacheManager; + private readonly ISignals _signals; + + public SecureSocketsLayerService( + IWorkContextAccessor workContextAccessor, + ICacheManager cacheManager, + ISignals signals) { + _workContextAccessor = workContextAccessor; + _cacheManager = cacheManager; + _signals = signals; + } + + public bool ShouldBeSecure(string actionName, string controllerName, RouteValueDictionary routeValues) { + var requestContext = GetRequestContext(actionName, controllerName, routeValues); + return ShouldBeSecure(requestContext, null); + } + + public bool ShouldBeSecure(RequestContext requestContext) { + return ShouldBeSecure(requestContext, null); + } + + public bool ShouldBeSecure(ActionExecutingContext actionContext) { + var requestContext = GetRequestContext( + actionContext.ActionDescriptor.ActionName, + actionContext.ActionDescriptor.ControllerDescriptor.ControllerName, + actionContext.RequestContext.RouteData.Values); + return ShouldBeSecure(requestContext, actionContext); + } + + private bool ShouldBeSecure(RequestContext requestContext, ActionExecutingContext actionContext) { + var controllerName = (string) requestContext.RouteData.Values["controller"]; + if (controllerName == null) return false; + var actionName = (string) requestContext.RouteData.Values["action"]; + if (actionName == null) return false; + + if (actionName.EndsWith("Ssl") || controllerName.EndsWith("Ssl")) { + return true; + } + + var controller = (actionContext != null + ? actionContext.Controller + : ControllerBuilder.Current.GetControllerFactory() + .CreateController(requestContext, controllerName)) as ControllerBase; + if (controller != null) { + var controllerType = controller.GetType(); + if (controllerType.GetCustomAttributes(typeof(RequireHttpsAttribute), false).Any()) { + return true; + } + ActionDescriptor actionDescriptor; + if (actionContext != null) { + actionDescriptor = actionContext.ActionDescriptor; + } + else { + var controllerContext = new ControllerContext(requestContext, controller); + var controllerDescriptor = new ReflectedControllerDescriptor(controllerType); + actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName); + } + if (actionDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), false).Any()) { + return true; + } + } + + var settings = GetSettings(); + if (settings == null) return false; + + if (settings.SecureEverything) return true; + + if (controllerName == "Account" && + (actionName == "LogOn" + || actionName == "ChangePassword" + || actionName == "AccessDenied" + || actionName == "Register" + || actionName.StartsWith("ChallengeEmail", StringComparison.OrdinalIgnoreCase))) { + return true; + } + + if (controllerName == "Admin" || AdminFilter.IsApplied(requestContext)) { + return true; + } + + if (!settings.CustomEnabled) return false; + + var urlHelper = new UrlHelper(requestContext); + var url = urlHelper.Action(actionName, controllerName, requestContext.RouteData); + + return IsRequestProtected( + url, requestContext.HttpContext.Request.ApplicationPath, settings); + } + + public string SecureActionUrl(string actionName, string controllerName) { + return SecureActionUrl(actionName, controllerName, new object()); + } + + public string SecureActionUrl(string actionName, string controllerName, object routeValues) { + return SecureActionUrl(actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + public string SecureActionUrl(string actionName, string controllerName, RouteValueDictionary routeValues) { + var requestContext = GetRequestContext(actionName, controllerName, routeValues); + var url = new UrlHelper(requestContext); + var actionUrl = url.Action(actionName, controllerName, routeValues); + if (actionUrl == null) return null; + var currentUri = _workContextAccessor.GetContext().HttpContext.Request.Url; + return currentUri != null && currentUri.Scheme.Equals(Uri.UriSchemeHttps) ? + actionUrl /* action url is relative so will keep current protocol */ : + MakeSecure(actionUrl); + } + + public string InsecureActionUrl(string actionName, string controllerName) { + return InsecureActionUrl(actionName, controllerName, new object()); + } + + public string InsecureActionUrl(string actionName, string controllerName, object routeValues) { + return InsecureActionUrl(actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + public string InsecureActionUrl(string actionName, string controllerName, RouteValueDictionary routeValues) { + var requestContext = GetRequestContext(actionName, controllerName, routeValues); + var url = new UrlHelper(requestContext); + var actionUrl = url.Action(actionName, controllerName, routeValues); + if (actionUrl == null) return null; + var currentUri = _workContextAccessor.GetContext().HttpContext.Request.Url; + return currentUri != null && currentUri.Scheme.Equals(Uri.UriSchemeHttp) ? + actionUrl /* action url is relative so will keep current protocol */ : + MakeInsecure(actionUrl); + } + + private RequestContext GetRequestContext( + string actionName, + string controllerName, + RouteValueDictionary routeValues) { + + var httpContext = _workContextAccessor.GetContext().HttpContext; + var routeData = new RouteData(); + foreach (var routeValue in routeValues) { + routeData.Values[routeValue.Key] = routeValue.Value; + } + routeData.Values["controller"] = controllerName; + routeData.Values["action"] = actionName; + var requestContext = new RequestContext(httpContext, routeData); + return requestContext; + } + + private static bool IsRequestProtected(string path, string appPath, SslSettings settings) { + var match = false; + var sr = new StringReader(settings.Urls ?? ""); + string pattern; + + while (!match && null != (pattern = sr.ReadLine())) { + pattern = pattern.Trim(); + match = IsMatched(pattern, path, appPath); + } + + return match; + } + + private static bool IsMatched(string pattern, string path, string appPath) { + if (pattern.StartsWith("~/")) { + pattern = pattern.Substring(2); + if (appPath == "/") + appPath = ""; + pattern = string.Format("{0}/{1}", appPath, pattern); + } + + if (!pattern.Contains("?")) + pattern = pattern.TrimEnd('/'); + + var requestPath = path; + if (!requestPath.Contains("?")) + requestPath = requestPath.TrimEnd('/'); + + return pattern.EndsWith("*") + ? requestPath.StartsWith(pattern.TrimEnd('*'), StringComparison.OrdinalIgnoreCase) + : string.Equals(requestPath, pattern, StringComparison.OrdinalIgnoreCase); + } + + private SslSettings GetSettings() { + return _cacheManager.Get("SslSettings", + ctx => { + ctx.Monitor(_signals.When(SslSettingsPart.CacheKey)); + var settingsPart = _workContextAccessor.GetContext().CurrentSite.As(); + return new SslSettings { + Urls = settingsPart.Urls, + CustomEnabled = settingsPart.CustomEnabled, + SecureEverything = settingsPart.SecureEverything, + SecureHostName = settingsPart.SecureHostName, + InsecureHostName = settingsPart.InsecureHostName + }; + }); + } + + private string MakeInsecure(string path) { + var settings = GetSettings(); + if (settings == null) return path; + var builder = new UriBuilder { + Scheme = Uri.UriSchemeHttp, + Port = 80 + }; + var insecureHostName = settings.InsecureHostName; + SetHost(insecureHostName, builder); + builder.Path = path; + return builder.Uri.ToString(); + } + + private string MakeSecure(string path) { + var settings = GetSettings(); + if (settings == null) return path; + var builder = new UriBuilder { + Scheme = Uri.UriSchemeHttps, + Port = 443 + }; + var secureHostName = settings.SecureHostName; + SetHost(secureHostName, builder); + builder.Path = path; + return builder.Uri.ToString(); + } + + private static void SetHost(string hostName, UriBuilder builder) { + if (string.IsNullOrWhiteSpace(hostName)) return; + var splitSecuredHostName = hostName.Split(new[] {':'}, StringSplitOptions.RemoveEmptyEntries); + if (splitSecuredHostName.Length == 2) { + int port; + if (int.TryParse(splitSecuredHostName[1], NumberStyles.Integer, CultureInfo.InvariantCulture, + out port)) { + builder.Port = port; + hostName = splitSecuredHostName[0]; + } + } + builder.Host = hostName; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Views/EditorTemplates/Parts/SecureSocketsLayer.Settings.cshtml b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Views/EditorTemplates/Parts/SecureSocketsLayer.Settings.cshtml new file mode 100644 index 000000000..9433f52e4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.SecureSocketsLayer/Views/EditorTemplates/Parts/SecureSocketsLayer.Settings.cshtml @@ -0,0 +1,36 @@ +@model Orchard.SecureSocketsLayer.Models.SslSettingsPart + +
+ @T("SSL Settings") +
+ @Html.EditorFor(m => m.SecureEverything) + + @Html.ValidationMessage("SecureEverything", "*") +
+
+
+ @Html.EditorFor(m => m.CustomEnabled) + + @Html.ValidationMessage("CustomEnabled", "*") +
+
+
+ + @Html.TextAreaFor(m => m.Urls, new { @class = "textMedium", rows = "5" } ) + @Html.ValidationMessage("Urls", "*") + @T("Provide a list of urls, one per line. Urls can contains wildcard matches using '*', or root identifier like '~/'") + @T("Examples: http://mysite.com/mypage, ~/Profile/Edit, ~/Profile/*") +
+
+
+
+ + @Html.TextBoxFor(m => m.SecureHostName, new { @class = "textMedium" }) + @T("Provide the host name secure traffic should be redirected to (e.g. secure.mydomain.com). Don't include the protocol or anything else than the host name. A port can be specified after a colon if necessary (e.g. secure.127-0-0-1.org.uk:4333). Leave empty to remain on the same host name (not recommended as this can be a security issue).") +
+
+ + @Html.TextBoxFor(m => m.InsecureHostName, new { @class = "textMedium" }) + @T("Provide the host name non-secured traffic should be redirected to (e.g. mydomain.com). Don't include the protocol or anything else than the host name. A port can be specified after a colon if necessary (e.g. dev.127-0-0-1.org.uk:4333). Leave empty to remain on the same host name (not recommended as this can be a security issue).") +
+
\ No newline at end of file diff --git a/src/Orchard.sln b/src/Orchard.sln index 378128adc..c9f77712a 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -158,6 +158,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules.Deprecated", "Modul EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Azure", "Orchard.Web\Modules\Orchard.Azure\Orchard.Azure.csproj", "{CBC7993C-57D8-4A6C-992C-19E849DFE71D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.SecureSocketsLayer", "Orchard.Web\Modules\Orchard.SecureSocketsLayer\Orchard.SecureSocketsLayer.csproj", "{36B82383-D69E-4897-A24A-648BABDF80EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeCoverage|Any CPU = CodeCoverage|Any CPU @@ -876,6 +878,16 @@ Global {CBC7993C-57D8-4A6C-992C-19E849DFE71D}.FxCop|Any CPU.Build.0 = Release|Any CPU {CBC7993C-57D8-4A6C-992C-19E849DFE71D}.Release|Any CPU.ActiveCfg = Release|Any CPU {CBC7993C-57D8-4A6C-992C-19E849DFE71D}.Release|Any CPU.Build.0 = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.Coverage|Any CPU.Build.0 = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.FxCop|Any CPU.Build.0 = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36B82383-D69E-4897-A24A-648BABDF80EC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -934,6 +946,7 @@ Global {6E444FF1-A47C-4CF6-BB3F-507C8EBD776D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {8A9FDB57-342D-49C2-BAFC-D885AAE5CC7C} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {CBC7993C-57D8-4A6C-992C-19E849DFE71D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {36B82383-D69E-4897-A24A-648BABDF80EC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} diff --git a/src/Orchard/ContentManagement/FieldStorage/InfosetStorage/InfosetPart.cs b/src/Orchard/ContentManagement/FieldStorage/InfosetStorage/InfosetPart.cs index dc1694b73..f5431eb49 100644 --- a/src/Orchard/ContentManagement/FieldStorage/InfosetStorage/InfosetPart.cs +++ b/src/Orchard/ContentManagement/FieldStorage/InfosetStorage/InfosetPart.cs @@ -67,10 +67,10 @@ namespace Orchard.ContentManagement.FieldStorage.InfosetStorage { partElement.Add(fieldElement); } if (string.IsNullOrEmpty(valueName)) { - fieldElement.Value = value; + fieldElement.Value = value ?? ""; } else { - fieldElement.SetAttributeValue(XmlConvert.EncodeLocalName(valueName), value); + fieldElement.SetAttributeValue(XmlConvert.EncodeLocalName(valueName), value ?? ""); } } }